From 1d51328ba0e823d872211e10b33ba87fcd677a23 Mon Sep 17 00:00:00 2001
From: Ron Lucke <lucke@elan-ev.de>
Date: Wed, 8 Nov 2023 07:53:25 +0000
Subject: TIC 3361 - Courseware CSS aufteilen

Closes #3361

Merge request studip/studip!2293
---
 resources/assets/stylesheets/scss/courseware.scss  | 5855 +-------------------
 .../assets/stylesheets/scss/courseware/a11y.scss   |   10 +
 .../stylesheets/scss/courseware/blockadder.scss    |  319 ++
 .../stylesheets/scss/courseware/blocks/audio.scss  |  231 +
 .../scss/courseware/blocks/biography.scss          |   82 +
 .../stylesheets/scss/courseware/blocks/canvas.scss |  145 +
 .../stylesheets/scss/courseware/blocks/code.scss   |   88 +
 .../scss/courseware/blocks/confirm.scss            |   12 +
 .../stylesheets/scss/courseware/blocks/date.scss   |   79 +
 .../scss/courseware/blocks/default-block.scss      |  134 +
 .../scss/courseware/blocks/dialog-cards.scss       |  150 +
 .../scss/courseware/blocks/document.scss           |  176 +
 .../stylesheets/scss/courseware/blocks/embed.scss  |   15 +
 .../stylesheets/scss/courseware/blocks/files.scss  |  192 +
 .../scss/courseware/blocks/gallery.scss            |  160 +
 .../scss/courseware/blocks/headline.scss           |  651 +++
 .../stylesheets/scss/courseware/blocks/iframe.scss |   54 +
 .../scss/courseware/blocks/image-map.scss          |   35 +
 .../scss/courseware/blocks/keypoint.scss           |  100 +
 .../stylesheets/scss/courseware/blocks/link.scss   |   73 +
 .../stylesheets/scss/courseware/blocks/lti.scss    |   20 +
 .../scss/courseware/blocks/table-of-contents.scss  |   71 +
 .../stylesheets/scss/courseware/blocks/text.scss   |   16 +
 .../scss/courseware/blocks/timeline.scss           |  244 +
 .../scss/courseware/blocks/typewriter.scss         |   38 +
 .../stylesheets/scss/courseware/comments.scss      |   57 +
 .../scss/courseware/containers/accordion.scss      |    0
 .../courseware/containers/default-container.scss   |  202 +
 .../scss/courseware/containers/list.scss           |    0
 .../scss/courseware/containers/tabs.scss           |    0
 .../scss/courseware/content-courses.scss           |   12 +
 .../stylesheets/scss/courseware/dashboard.scss     |  131 +
 .../stylesheets/scss/courseware/gradients.scss     |  150 +
 .../scss/courseware/layouts/collapsible.scss       |   46 +
 .../scss/courseware/layouts/companion.scss         |   90 +
 .../scss/courseware/layouts/import-zip.scss        |   22 +
 .../scss/courseware/layouts/input-file.scss        |   36 +
 .../scss/courseware/layouts/progress.scss          |  160 +
 .../scss/courseware/layouts/ribbon.scss            |  359 ++
 .../stylesheets/scss/courseware/layouts/tabs.scss  |  114 +
 .../scss/courseware/layouts/talk-bubble.scss       |   58 +
 .../stylesheets/scss/courseware/layouts/tile.scss  |  157 +
 .../stylesheets/scss/courseware/layouts/tree.scss  |  163 +
 .../assets/stylesheets/scss/courseware/shelf.scss  |   53 +
 .../stylesheets/scss/courseware/sortable.scss      |   57 +
 .../scss/courseware/structural-element.scss        |  273 +
 .../stylesheets/scss/courseware/variables.scss     |  205 +
 .../stylesheets/scss/courseware/widgets.scss       |   56 +
 .../stylesheets/scss/courseware/wizards.scss       |   57 +
 .../stylesheets/scss/courseware_gradients.scss     |  150 -
 .../stylesheets/scss/progress_indicator.scss       |    4 +
 resources/vue/components/blubber/Panel.vue         |    2 +-
 .../vue/components/courseware/ActivitiesApp.vue    |    4 +-
 resources/vue/components/courseware/AdminApp.vue   |    4 +-
 .../vue/components/courseware/CommentsApp.vue      |    6 +-
 .../components/courseware/ContentReleasesApp.vue   |    2 +-
 .../courseware/CoursewareAccordionContainer.vue    |  405 --
 .../courseware/CoursewareActionWidget.vue          |   42 -
 .../components/courseware/CoursewareActivities.vue |    2 +-
 .../CoursewareActivitiesWidgetFilterType.vue       |   61 -
 .../CoursewareActivitiesWidgetFilterUnit.vue       |   58 -
 .../courseware/CoursewareAdminActionWidget.vue     |   34 -
 .../courseware/CoursewareAdminViewWidget.vue       |   33 -
 .../components/courseware/CoursewareAudioBlock.vue |  636 ---
 .../courseware/CoursewareBeforeAfterBlock.vue      |  287 -
 .../CoursewareBiographyAchievementsBlock.vue       |  200 -
 .../courseware/CoursewareBiographyCareerBlock.vue  |  262 -
 .../courseware/CoursewareBiographyGoalsBlock.vue   |  136 -
 ...CoursewareBiographyPersonalInformationBlock.vue |  185 -
 .../courseware/CoursewareBlockActions.vue          |  151 -
 .../courseware/CoursewareBlockAdderArea.vue        |   72 -
 .../courseware/CoursewareBlockComments.vue         |  138 -
 .../courseware/CoursewareBlockDiscussion.vue       |   60 -
 .../components/courseware/CoursewareBlockEdit.vue  |   50 -
 .../courseware/CoursewareBlockExportOptions.vue    |   18 -
 .../courseware/CoursewareBlockFeedback.vue         |  141 -
 .../courseware/CoursewareBlockHelper.vue           |  256 -
 .../components/courseware/CoursewareBlockInfo.vue  |   65 -
 .../courseware/CoursewareBlockadderItem.vue        |  115 -
 .../courseware/CoursewareCanvasBlock.vue           |  712 ---
 .../components/courseware/CoursewareChartBlock.vue |  333 --
 .../courseware/CoursewareClipboardItem.vue         |  246 -
 .../components/courseware/CoursewareCodeBlock.vue  |  117 -
 .../courseware/CoursewareCollapsibleBox.vue        |   57 -
 .../CoursewareCommentsOverviewDialog.vue           |   10 +-
 ...ursewareCommentsOverviewWidgetFilterCreated.vue |   51 -
 .../CoursewareCommentsOverviewWidgetFilterType.vue |   51 -
 .../CoursewareCommentsOverviewWidgetFilterUnit.vue |   60 -
 .../courseware/CoursewareCompanionBox.vue          |   24 -
 .../courseware/CoursewareCompanionOverlay.vue      |   49 -
 .../courseware/CoursewareConfirmBlock.vue          |  118 -
 .../courseware/CoursewareContainerActions.vue      |   88 -
 .../courseware/CoursewareContainerAdderItem.vue    |   52 -
 .../courseware/CoursewareDashboardStudents.vue     |    4 +-
 .../courseware/CoursewareDashboardTasks.vue        |    8 +-
 .../components/courseware/CoursewareDateBlock.vue  |  204 -
 .../components/courseware/CoursewareDateInput.vue  |   34 -
 .../courseware/CoursewareDefaultBlock.vue          |  387 --
 .../courseware/CoursewareDefaultBlockElements.vue  |   47 -
 .../courseware/CoursewareDefaultContainer.vue      |  392 --
 .../courseware/CoursewareDialogCardsBlock.vue      |  295 -
 .../courseware/CoursewareDocumentBlock.vue         |  771 ---
 .../courseware/CoursewareDownloadBlock.vue         |  296 -
 .../components/courseware/CoursewareEmbedBlock.vue |  252 -
 .../courseware/CoursewareEmptyElementBox.vue       |   58 -
 .../components/courseware/CoursewareErrorBlock.vue |   56 -
 .../courseware/CoursewareExportWidget.vue          |   95 -
 .../courseware/CoursewareFileChooser.vue           |  139 -
 .../courseware/CoursewareFolderBlock.vue           |  364 --
 .../courseware/CoursewareFolderChooser.vue         |  167 -
 .../courseware/CoursewareGalleryBlock.vue          |  395 --
 .../courseware/CoursewareHeadlineBlock.vue         |  634 ---
 .../courseware/CoursewareIframeBlock.vue           |  266 -
 .../courseware/CoursewareImageMapBlock.vue         |  787 ---
 .../courseware/CoursewareImportWidget.vue          |   60 -
 .../courseware/CoursewareKeyPointBlock.vue         |  179 -
 .../components/courseware/CoursewareLinkBlock.vue  |  152 -
 .../courseware/CoursewareListContainer.vue         |  287 -
 .../components/courseware/CoursewareLtiBlock.vue   |  279 -
 .../courseware/CoursewarePDFTableOfContent.vue     |   30 -
 .../courseware/CoursewareProgressCircle.vue        |   18 -
 .../vue/components/courseware/CoursewareRibbon.vue |  170 -
 .../courseware/CoursewareRibbonToolbar.vue         |  206 -
 .../courseware/CoursewareSearchResults.vue         |   80 -
 .../courseware/CoursewareSearchWidget.vue          |   94 -
 .../courseware/CoursewareSharedItems.vue           |   88 -
 .../courseware/CoursewareShelfActionWidget.vue     |   30 -
 .../courseware/CoursewareShelfDialogAdd.vue        |  317 --
 .../courseware/CoursewareShelfDialogCopy.vue       |  401 --
 .../courseware/CoursewareShelfDialogImport.vue     |  373 --
 .../courseware/CoursewareShelfImportWidget.vue     |   37 -
 .../courseware/CoursewareStructuralElement.vue     | 1762 ------
 .../CoursewareStructuralElementComments.vue        |  139 -
 .../CoursewareStructuralElementDialogAdd.vue       |  380 --
 .../CoursewareStructuralElementDialogCopy.vue      |  477 --
 .../CoursewareStructuralElementDialogImport.vue    |  200 -
 .../CoursewareStructuralElementDialogLink.vue      |  232 -
 .../CoursewareStructuralElementDiscussion.vue      |   60 -
 .../CoursewareStructuralElementFeedback.vue        |  141 -
 .../CoursewareStructuralElementPermissions.vue     |  471 --
 .../CoursewareStructuralElementSelector.vue        |  141 -
 .../CoursewareStructuralElementSelectorItem.vue    |  272 -
 .../vue/components/courseware/CoursewareTab.vue    |   51 -
 .../courseware/CoursewareTableOfContentsBlock.vue  |  234 -
 .../vue/components/courseware/CoursewareTabs.vue   |  133 -
 .../courseware/CoursewareTabsContainer.vue         |  403 --
 .../components/courseware/CoursewareTalkBubble.vue |   26 -
 .../courseware/CoursewareTasksActionWidget.vue     |   31 -
 .../courseware/CoursewareTasksDialogDistribute.vue |    6 +-
 .../components/courseware/CoursewareTextBlock.vue  |  113 -
 .../vue/components/courseware/CoursewareTile.vue   |  145 -
 .../courseware/CoursewareTimelineBlock.vue         |  312 --
 .../courseware/CoursewareToolsBlockadder.vue       |  437 --
 .../courseware/CoursewareToolsContents.vue         |   23 -
 .../vue/components/courseware/CoursewareTree.vue   |  248 -
 .../components/courseware/CoursewareTreeItem.vue   |  473 --
 .../courseware/CoursewareTypewriterBlock.vue       |  156 -
 .../components/courseware/CoursewareUnitItem.vue   |  197 -
 .../courseware/CoursewareUnitItemDialogExport.vue  |  132 -
 .../courseware/CoursewareUnitItemDialogLayout.vue  |  197 -
 .../CoursewareUnitItemDialogSettings.vue           |  311 --
 .../components/courseware/CoursewareUnitItems.vue  |   80 -
 .../courseware/CoursewareUnitProgress.vue          |  118 -
 .../courseware/CoursewareUnitProgressItem.vue      |   31 -
 .../components/courseware/CoursewareVideoBlock.vue |  242 -
 .../components/courseware/CoursewareViewWidget.vue |   81 -
 .../courseware/CoursewareWelcomeScreen.vue         |   88 -
 resources/vue/components/courseware/IndexApp.vue   |   21 +-
 resources/vue/components/courseware/IsoDate.vue    |   17 -
 resources/vue/components/courseware/PublicApp.vue  |    6 +-
 .../PublicCoursewareStructuralElement.vue          |  256 -
 resources/vue/components/courseware/ShelfApp.vue   |   16 +-
 resources/vue/components/courseware/TasksApp.vue   |    4 +-
 resources/vue/components/courseware/block-mixin.js |   35 -
 .../courseware/blocks/CoursewareAudioBlock.vue     |  633 +++
 .../blocks/CoursewareBeforeAfterBlock.vue          |  288 +
 .../CoursewareBiographyAchievementsBlock.vue       |  200 +
 .../blocks/CoursewareBiographyCareerBlock.vue      |  258 +
 .../blocks/CoursewareBiographyGoalsBlock.vue       |  136 +
 ...CoursewareBiographyPersonalInformationBlock.vue |  186 +
 .../courseware/blocks/CoursewareBlockActions.vue   |  151 +
 .../courseware/blocks/CoursewareBlockComments.vue  |  138 +
 .../blocks/CoursewareBlockDiscussion.vue           |   60 +
 .../courseware/blocks/CoursewareBlockEdit.vue      |   50 +
 .../blocks/CoursewareBlockExportOptions.vue        |   18 +
 .../courseware/blocks/CoursewareBlockFeedback.vue  |  141 +
 .../courseware/blocks/CoursewareBlockInfo.vue      |   65 +
 .../courseware/blocks/CoursewareCanvasBlock.vue    |  705 +++
 .../courseware/blocks/CoursewareChartBlock.vue     |  325 ++
 .../courseware/blocks/CoursewareCodeBlock.vue      |  120 +
 .../courseware/blocks/CoursewareConfirmBlock.vue   |  119 +
 .../courseware/blocks/CoursewareDateBlock.vue      |  205 +
 .../courseware/blocks/CoursewareDefaultBlock.vue   |  389 ++
 .../blocks/CoursewareDialogCardsBlock.vue          |  291 +
 .../courseware/blocks/CoursewareDocumentBlock.vue  |  809 +++
 .../courseware/blocks/CoursewareDownloadBlock.vue  |  286 +
 .../courseware/blocks/CoursewareEmbedBlock.vue     |  252 +
 .../courseware/blocks/CoursewareErrorBlock.vue     |   52 +
 .../courseware/blocks/CoursewareFolderBlock.vue    |  377 ++
 .../courseware/blocks/CoursewareGalleryBlock.vue   |  400 ++
 .../courseware/blocks/CoursewareHeadlineBlock.vue  |  631 +++
 .../courseware/blocks/CoursewareIframeBlock.vue    |  258 +
 .../courseware/blocks/CoursewareImageMapBlock.vue  |  804 +++
 .../courseware/blocks/CoursewareKeyPointBlock.vue  |  184 +
 .../courseware/blocks/CoursewareLinkBlock.vue      |  152 +
 .../courseware/blocks/CoursewareLtiBlock.vue       |  300 +
 .../blocks/CoursewarePDFTableOfContent.vue         |   30 +
 .../blocks/CoursewareTableOfContentsBlock.vue      |  251 +
 .../courseware/blocks/CoursewareTextBlock.vue      |  115 +
 .../courseware/blocks/CoursewareTimelineBlock.vue  |  307 +
 .../blocks/CoursewareTypewriterBlock.vue           |  156 +
 .../courseware/blocks/CoursewareVideoBlock.vue     |  241 +
 .../courseware/blocks/block-components.js          |   18 +
 .../components/courseware/container-components.js  |   72 -
 .../containers/CoursewareAccordionContainer.vue    |  403 ++
 .../containers/CoursewareBlockAdderArea.vue        |   72 +
 .../containers/CoursewareContainerActions.vue      |   88 +
 .../containers/CoursewareContainerAdderItem.vue    |   52 +
 .../containers/CoursewareDefaultContainer.vue      |  392 ++
 .../containers/CoursewareListContainer.vue         |  285 +
 .../containers/CoursewareTabsContainer.vue         |  399 ++
 .../courseware/containers/container-components.js  |   77 +
 .../vue/components/courseware/content-icons.js     |  114 -
 .../layouts/CoursewareCollapsibleBox.vue           |   57 +
 .../courseware/layouts/CoursewareCompanionBox.vue  |   24 +
 .../layouts/CoursewareCompanionOverlay.vue         |   49 +
 .../courseware/layouts/CoursewareDateInput.vue     |   34 +
 .../courseware/layouts/CoursewareFileChooser.vue   |  139 +
 .../courseware/layouts/CoursewareFolderChooser.vue |  167 +
 .../courseware/layouts/CoursewareTab.vue           |   51 +
 .../courseware/layouts/CoursewareTabs.vue          |  133 +
 .../courseware/layouts/CoursewareTalkBubble.vue    |   26 +
 .../courseware/layouts/CoursewareTile.vue          |  145 +
 .../vue/components/courseware/layouts/IsoDate.vue  |   17 +
 .../vue/components/courseware/plugin-components.js |   16 +-
 .../CoursewareBlockadderItem.vue                   |  115 +
 .../structural-element/CoursewareClipboardItem.vue |  246 +
 .../CoursewareEmptyElementBox.vue                  |   58 +
 .../structural-element/CoursewareRibbon.vue        |  170 +
 .../structural-element/CoursewareRibbonToolbar.vue |  206 +
 .../structural-element/CoursewareSearchResults.vue |   80 +
 .../CoursewareStructuralElement.vue                | 1743 ++++++
 .../CoursewareStructuralElementComments.vue        |  139 +
 .../CoursewareStructuralElementDialogAdd.vue       |  380 ++
 .../CoursewareStructuralElementDialogCopy.vue      |  477 ++
 .../CoursewareStructuralElementDialogImport.vue    |  200 +
 .../CoursewareStructuralElementDialogLink.vue      |  232 +
 .../CoursewareStructuralElementDiscussion.vue      |   60 +
 .../CoursewareStructuralElementFeedback.vue        |  141 +
 .../CoursewareStructuralElementPermissions.vue     |  471 ++
 .../CoursewareStructuralElementSelector.vue        |  141 +
 .../CoursewareStructuralElementSelectorItem.vue    |  272 +
 .../CoursewareToolsBlockadder.vue                  |  436 ++
 .../structural-element/CoursewareToolsContents.vue |   23 +
 .../structural-element/CoursewareTree.vue          |  248 +
 .../structural-element/CoursewareTreeItem.vue      |  473 ++
 .../structural-element/CoursewareWelcomeScreen.vue |   88 +
 .../PublicCoursewareStructuralElement.vue          |  236 +
 .../structural-element-components.js               |   31 +
 .../courseware/unit/CoursewareProgressCircle.vue   |   18 +
 .../courseware/unit/CoursewareSharedItems.vue      |   88 +
 .../courseware/unit/CoursewareShelfDialogAdd.vue   |  317 ++
 .../courseware/unit/CoursewareShelfDialogCopy.vue  |  401 ++
 .../unit/CoursewareShelfDialogImport.vue           |  373 ++
 .../courseware/unit/CoursewareUnitItem.vue         |  197 +
 .../unit/CoursewareUnitItemDialogExport.vue        |  132 +
 .../unit/CoursewareUnitItemDialogLayout.vue        |  197 +
 .../unit/CoursewareUnitItemDialogSettings.vue      |  311 ++
 .../courseware/unit/CoursewareUnitItems.vue        |   80 +
 .../courseware/unit/CoursewareUnitProgress.vue     |  118 +
 .../courseware/unit/CoursewareUnitProgressItem.vue |   31 +
 .../courseware/widgets/CoursewareActionWidget.vue  |   42 +
 .../CoursewareActivitiesWidgetFilterType.vue       |   61 +
 .../CoursewareActivitiesWidgetFilterUnit.vue       |   58 +
 .../widgets/CoursewareAdminActionWidget.vue        |   34 +
 .../widgets/CoursewareAdminViewWidget.vue          |   33 +
 ...ursewareCommentsOverviewWidgetFilterCreated.vue |   51 +
 .../CoursewareCommentsOverviewWidgetFilterType.vue |   51 +
 .../CoursewareCommentsOverviewWidgetFilterUnit.vue |   60 +
 .../courseware/widgets/CoursewareExportWidget.vue  |   95 +
 .../courseware/widgets/CoursewareImportWidget.vue  |   60 +
 .../courseware/widgets/CoursewareSearchWidget.vue  |   94 +
 .../widgets/CoursewareShelfActionWidget.vue        |   30 +
 .../widgets/CoursewareShelfImportWidget.vue        |   37 +
 .../widgets/CoursewareTasksActionWidget.vue        |   31 +
 .../courseware/widgets/CoursewareViewWidget.vue    |   81 +
 resources/vue/courseware-index-app.js              |    2 +-
 resources/vue/courseware-public-app.js             |    2 +-
 resources/vue/mixins/courseware/block.js           |   37 +
 resources/vue/mixins/courseware/content-icons.js   |  122 +
 290 files changed, 28258 insertions(+), 28870 deletions(-)
 create mode 100644 resources/assets/stylesheets/scss/courseware/a11y.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blockadder.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/audio.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/biography.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/canvas.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/code.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/confirm.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/date.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/default-block.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/dialog-cards.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/document.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/embed.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/files.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/gallery.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/headline.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/iframe.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/image-map.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/keypoint.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/link.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/lti.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/table-of-contents.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/text.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/timeline.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/blocks/typewriter.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/comments.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/containers/accordion.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/containers/default-container.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/containers/list.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/containers/tabs.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/content-courses.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/dashboard.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/gradients.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/collapsible.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/companion.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/import-zip.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/input-file.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/progress.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/tabs.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/talk-bubble.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/tile.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/tree.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/shelf.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/sortable.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/structural-element.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/variables.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/widgets.scss
 create mode 100644 resources/assets/stylesheets/scss/courseware/wizards.scss
 delete mode 100644 resources/assets/stylesheets/scss/courseware_gradients.scss
 delete mode 100644 resources/vue/components/courseware/CoursewareAccordionContainer.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareActionWidget.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareActivitiesWidgetFilterType.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareActivitiesWidgetFilterUnit.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareAdminActionWidget.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareAdminViewWidget.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareAudioBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBiographyAchievementsBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBiographyCareerBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBiographyGoalsBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBiographyPersonalInformationBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBlockActions.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBlockAdderArea.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBlockComments.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBlockDiscussion.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBlockEdit.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBlockExportOptions.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBlockFeedback.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBlockHelper.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBlockInfo.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareBlockadderItem.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareCanvasBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareChartBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareClipboardItem.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareCodeBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareCollapsibleBox.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterCreated.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterType.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterUnit.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareCompanionBox.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareCompanionOverlay.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareConfirmBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareContainerActions.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareContainerAdderItem.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareDateBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareDateInput.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareDefaultBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareDefaultBlockElements.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareDefaultContainer.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareDialogCardsBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareDocumentBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareDownloadBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareEmbedBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareEmptyElementBox.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareErrorBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareExportWidget.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareFileChooser.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareFolderBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareFolderChooser.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareGalleryBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareHeadlineBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareIframeBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareImageMapBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareImportWidget.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareKeyPointBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareLinkBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareListContainer.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareLtiBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewarePDFTableOfContent.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareProgressCircle.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareRibbon.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareRibbonToolbar.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareSearchResults.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareSearchWidget.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareSharedItems.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareShelfActionWidget.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareShelfDialogAdd.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareShelfDialogCopy.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareShelfDialogImport.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareShelfImportWidget.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElement.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElementComments.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElementDialogAdd.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElementDialogImport.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElementSelector.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareStructuralElementSelectorItem.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTab.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTabs.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTabsContainer.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTalkBubble.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTasksActionWidget.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTextBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTile.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTimelineBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareToolsBlockadder.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareToolsContents.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTree.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTreeItem.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareTypewriterBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareUnitItem.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareUnitItemDialogExport.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareUnitItemDialogLayout.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareUnitItemDialogSettings.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareUnitItems.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareUnitProgress.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareUnitProgressItem.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareVideoBlock.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareViewWidget.vue
 delete mode 100644 resources/vue/components/courseware/CoursewareWelcomeScreen.vue
 delete mode 100644 resources/vue/components/courseware/IsoDate.vue
 delete mode 100644 resources/vue/components/courseware/PublicCoursewareStructuralElement.vue
 delete mode 100644 resources/vue/components/courseware/block-mixin.js
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBiographyPersonalInformationBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBlockComments.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBlockExportOptions.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBlockFeedback.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareBlockInfo.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareChartBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareCodeBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareDateBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareEmbedBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareErrorBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareLinkBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareLtiBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewarePDFTableOfContent.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareTextBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareTimelineBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareTypewriterBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue
 create mode 100644 resources/vue/components/courseware/blocks/block-components.js
 delete mode 100644 resources/vue/components/courseware/container-components.js
 create mode 100644 resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue
 create mode 100644 resources/vue/components/courseware/containers/CoursewareBlockAdderArea.vue
 create mode 100644 resources/vue/components/courseware/containers/CoursewareContainerActions.vue
 create mode 100644 resources/vue/components/courseware/containers/CoursewareContainerAdderItem.vue
 create mode 100644 resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
 create mode 100644 resources/vue/components/courseware/containers/CoursewareListContainer.vue
 create mode 100644 resources/vue/components/courseware/containers/CoursewareTabsContainer.vue
 create mode 100644 resources/vue/components/courseware/containers/container-components.js
 delete mode 100644 resources/vue/components/courseware/content-icons.js
 create mode 100644 resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
 create mode 100644 resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue
 create mode 100644 resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
 create mode 100644 resources/vue/components/courseware/layouts/CoursewareDateInput.vue
 create mode 100644 resources/vue/components/courseware/layouts/CoursewareFileChooser.vue
 create mode 100644 resources/vue/components/courseware/layouts/CoursewareFolderChooser.vue
 create mode 100644 resources/vue/components/courseware/layouts/CoursewareTab.vue
 create mode 100644 resources/vue/components/courseware/layouts/CoursewareTabs.vue
 create mode 100644 resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue
 create mode 100644 resources/vue/components/courseware/layouts/CoursewareTile.vue
 create mode 100644 resources/vue/components/courseware/layouts/IsoDate.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareBlockadderItem.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareClipboardItem.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareEmptyElementBox.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogLink.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementDiscussion.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementPermissions.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelector.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelectorItem.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareToolsBlockadder.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareTree.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue
 create mode 100644 resources/vue/components/courseware/structural-element/CoursewareWelcomeScreen.vue
 create mode 100644 resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue
 create mode 100644 resources/vue/components/courseware/structural-element/structural-element-components.js
 create mode 100644 resources/vue/components/courseware/unit/CoursewareProgressCircle.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareSharedItems.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareShelfDialogImport.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareUnitItem.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareUnitItemDialogExport.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareUnitItems.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
 create mode 100644 resources/vue/components/courseware/unit/CoursewareUnitProgressItem.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareActionWidget.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareAdminActionWidget.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterUnit.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareExportWidget.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareImportWidget.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
 create mode 100644 resources/vue/components/courseware/widgets/CoursewareViewWidget.vue
 create mode 100644 resources/vue/mixins/courseware/block.js
 create mode 100644 resources/vue/mixins/courseware/content-icons.js

diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss
index b854bef..f5caa33 100644
--- a/resources/assets/stylesheets/scss/courseware.scss
+++ b/resources/assets/stylesheets/scss/courseware.scss
@@ -1,5831 +1,28 @@
-/* definitions */
 @use '../mixins.scss' as *;
-@import './drag-handle.scss';
-@import "courseware_gradients.scss";
 
-$companion-types: (
-    default: basic,
-    unsure: unsure,
-    special: special,
-    alert: alert,
-    sad: sad,
-    happy: happy,
-    pointing: pointing-right,
-    curious: curious
-);
-
-$element-icons: (
-    content: content2,
-    draft: category-draft,
-    task: category-task,
-    template: category-template,
-    oer: oer-campus,
-    other: category-others,
-    portfolio: category-portfolio
-);
-
-$tree-item-flag-icons: (
-    date: date,
-    write: edit,
-    cant-read: lock-locked2,
-);
-
-$tile-colors: (
-    black: #000,
-    charcoal: #3c454e,
-    royal-purple: #8656a2,
-    iguana-green: #66b570,
-    queen-blue: #536d96,
-    verdigris: #41afaa,
-    mulberry: #bf5796,
-    pumpkin: #f26e00,
-    sunglow: #ffca5c,
-    apple-green: #8bbd40,
-    studip-blue: #28497c,
-    studip-lightblue: #e7ebf1,
-    studip-red: #d60000,
-    studip-green: #008512,
-    studip-yellow: #ffbd33,
-    studip-gray: #636a71,
-);
-
-$icon-colors:(
-    icon-white: #ffffff,
-    icon-black: #000000,
-    icon-red: #cb1800,
-    icon-blue: #24437c,
-    icon-green: #00962d,
-    icon-gray: #6e6e6e,
-    icon-yellow: #ffad00
-);
-$border-colors:(
-    white: #ffffff,
-    black: #000000,
-    studip-red: #cb1800,
-    studip-blue: #24437c,
-    studip-green: #00962d,
-    studip-gray: #6e6e6e,
-    studip-yellow: #ffad00
-);
-
-$blockadder-items: (
-    before-after: block-comparison,
-    canvas: block-canvas,
-    gallery: block-gallery,
-    image-map: block-imagemap,
-    audio: audio,
-    chart: vote,
-    code: computer,
-    confirm: accept,
-    date: date,
-    dialog-cards: dialog-cards,
-    document: file-text,
-    download: download,
-    embed: code,
-    folder: folder-full,
-    headline: block-eyecatcher,
-    iframe: door-enter,
-    lti: plugin,
-    key-point: exclaim-circle,
-    link: link-extern,
-    table-of-contents: table-of-contents,
-    text: edit,
-    timeline: date-cycle,
-    typewriter: block-typewriter,
-    video: video2,
-    biography-achievements: medal,
-    biography-career: ranking,
-    biography-personal-information: own-license,
-    biography-goals: radar
-);
-
-$containeradder-items: (
-    accordion: block-accordion,
-    list: view-list,
-    tabs: block-tabs,
-);
-
-$achievement-types: (
-    certificate: file-text,
-    accreditation: vcard,
-    award: medal,
-    book: literature,
-    publication: news,
-    membership: group3,
-);
-
-$goals-types: (
-    personal: person2,
-    school: doctoral-cap,
-    academic: doctoral-cap,
-    professional: tools,
-);
-
-$media-buttons: (
-    play: play,
-    stop: stop,
-    pause: pause,
-    prev: arr_eol-left,
-    next: arr_eol-right
-);
-
-$cw-wrapper-gap: 0.5em;
-
-/* * * * * * * *
-c o n t e n t s
-* * * * * * * * */
-.cw-content-overview {
-    max-width: 1100px;
-    h2 {
-        margin: 0;
-        font-weight: 400;
-        padding: 5px 0;
-        font-size: 1.4em;
-    }
-}
-
-.cw-contents-overview-teaser {
-    max-width: 782px;
-    background-color: var(--content-color-20);
-    background-image: url("#{$image-path}/courseware-keyvisual-negative.svg");
-    background-repeat: no-repeat;
-    background-size: 196px;
-    background-position-y: 50%;
-    background-position-x: 24px;
-    padding: 24px;
-    margin-bottom: 10px;
-
-    .cw-contents-overview-teaser-content {
-        padding-left: 220px;
-
-        header{
-            font-size: 1.5em;
-            margin-bottom: 0.5em;
-        }
-    }
-}
-.responsive-display {
-    .cw-contents-overview-teaser {
-        max-width: 782px;
-        background-size: 60%;
-        background-position-y: 24px;
-        background-position-x: 50%;
-        padding: 24px;
-        margin-bottom: 10px;
-
-        .cw-contents-overview-teaser-content {
-            padding-top: 28%;
-            padding-left: 0;
-            text-align: justify;
-
-            header{
-                font-size: 1.5em;
-                margin: 1em 0 0.5em 0;
-                text-align: center;
-            }
-        }
-    }
-}
-
-.cw-loading-indicator-content {
-    margin-top: 76px;
-}
-.cw-content-loading {
-    /* Loading animation from activity feed */
-    .loading-indicator {
-        text-align: center;
-        padding: 1em 0;
-    }
-
-    .loading-indicator span {
-        background-color: var(--content-color-40);
-        border-radius: 50%;
-        height: 10px;
-        position: relative;
-        width: 10px;
-        display: inline-block;
-    }
-
-    .loading-indicator span.load-1 {
-        animation: loading-animation-1 1s linear 20;
-    }
-
-    .loading-indicator span.load-2 {
-        animation: loading-animation-2 1s linear 20;
-    }
-
-    .loading-indicator span.load-3 {
-        animation: loading-animation-3 1s linear 20;
-    }
-
-    @keyframes loading-animation-1 {
-      0%   { transform: scale(1); }
-      16%  { transform: scale(1.3); }
-      33%  { transform: scale(1); }
-      100% { transform: scale(1); }
-    }
-
-    @keyframes loading-animation-2 {
-      0%   { transform: scale(1); }
-      33%  { transform: scale(1); }
-      49%  { transform: scale(1.3); }
-      65%  { transform: scale(1); }
-      100% { transform: scale(1); }
-    }
-
-    @keyframes loading-animation-3 {
-      0%   { transform: scale(1); }
-      66%  { transform: scale(1); }
-      81%  { transform: scale(1.3); }
-      100% { transform: scale(1); }
-    }
-}
-
-.cw-content-courses {
-    h2 {
-        margin: 0;
-        font-weight: 400;
-        padding: 5px 0;
-        font-size: 1.4em;
-    }
-    ul.cw-tiles {
-        margin-bottom: 20px;
-    }
-}
-
-.cw-contents-overview-personal {
-    margin-bottom: 2em;
-}
-
-/* * * * * * * * * * *
-c o n t e n t s  e n d
-* * * * * * * * * * */
-
-/* * * * * *
-r i b b o n
-* * * * * */
-$consum_ribbon_width: calc(100% - 58px);
-#course-courseware-courseware,
-#contents-courseware-courseware,
-#contents-courseware-shared_content_courseware {
-    &.consume {
-        overflow: hidden;
-    }
-    #content-wrapper {
-        position: relative;
-    }
-}
-
-.cw-ribbon-wrapper-consume {
-    position: fixed;
-    padding: 15px;
-    background-color: var(--white);
-    width: $consum_ribbon_width;
-    height: 46px;
-    z-index: 42;
-}
-.cw-ribbon-consume-bottom {
-    position: fixed;
-    top: 75px;
-    height: 15px;
-    left: 0;
-    width: calc(100% - 1em);
-    background-color: var(--white);
-    z-index: 40;
-}
-
-.cw-ribbon-sticky-top {
-    position: fixed;
-    top: 40px;
-    height: 20px;
-    width: calc(100% - 314px);
-    background-color: var(--white);
-    z-index: 39;
-}
-.cw-ribbon-sticky-bottom {
-    position: fixed;
-    top: 110px;
-    height: 16px;
-    width: calc(100% - 314px);
-    background-color: var(--white);
-    z-index: 39;
-}
-.cw-ribbon-sticky-spacer {
-    height: 80px;
-}
-.cw-ribbon {
-    display: flex;
-    flex-wrap: wrap;
-    height: auto;
-    min-height: 30px;
-    border: solid thin var(--dark-gray-color-30);
-    margin-bottom: 15px;
-    padding: 1em;
-    justify-content: space-between;
-    background-color: var(--dark-gray-color-5);
-
-    &.cw-ribbon-sticky {
-        position: fixed;
-        top: 50px;
-        width: calc(100% - 346px);
-        z-index: 40;
-    }
-
-    &.cw-ribbon-consume {
-        width: $consum_ribbon_width;
-        position: fixed;
-        margin-bottom: 0;
-    }
-
-    .cw-ribbon-wrapper-left {
-        display: flex;
-        max-width: calc(100% - 106px);
-
-        .cw-ribbon-nav {
-            display: flex;
-            min-width: 75px;
-
-            &.single-icon {
-                min-width: 45px;
-            }
-        }
-
-        .cw-ribbon-breadcrumb {
-            font-size: 1.25em;
-            line-height: 1.5em;
-            margin-right: 1em;
-            min-width: 0;
-
-            ul {
-                display: flex;
-                list-style: none;
-                padding-left: 0;
-
-                li+li:before {
-                    padding: 0 0.25em;
-                    content: '/';
-                    background-repeat: no-repeat;
-                    background-position: center;
-                }
-
-                .cw-ribbon-breadcrumb-item {
-                    display: inline;
-                    flex-shrink: 100000;
-                    min-width: 0;
-                    overflow: hidden;
-                    text-overflow: ellipsis;
-                    white-space: nowrap;
-
-                    a {
-                        color: var(--base-color);
-                        text-decoration: none;
-                        &:hover {
-                            color: var(--active-color);
-                        }
-                    }
-
-                    img {
-                        vertical-align: text-top;
-                    }
-
-                    &.cw-ribbon-breadcrumb-item-current {
-                        flex-shrink: 1;
-                    }
-                }
-            }
-        }
-    }
-
-    .cw-ribbon-wrapper-right {
-        display: flex;
-
-        button {
-            border: none;
-            background-color: transparent;
-            cursor: pointer;
-        }
-    }
-
-    .cw-ribbon-button {
-        height: 24px;
-        width: 24px;
-        margin: 0 7px;
-        padding: 1px 2px;
-        border: none;
-        background-color: transparent;
-        background-repeat: no-repeat;
-        background-position: center;
-        background-size: 24px;
-        cursor: pointer;
-
-        &.cw-ribbon-button-menu {
-            @include background-icon(table-of-contents, clickable, 24);
-        }
-
-        &.cw-ribbon-button-prev {
-            @include background-icon(arr_1left, clickable, 24);
-            margin: 0 0.5em 0 0;
-        }
-
-        &.cw-ribbon-button-next {
-            @include background-icon(arr_1right, clickable, 24);
-            margin: 0 1em 0 0;
-        }
-
-        &.cw-ribbon-button-prev-disabled {
-            @include background-icon(arr_1left, inactive, 24);
-            margin: 0 0.5em 0 0;
-            cursor: default;
-        }
-
-        &.cw-ribbon-button-next-disabled {
-            @include background-icon(arr_1right, inactive, 24);
-            margin: 0 1em 0 0;
-            cursor: default;
-        }
-    }
-
-    .cw-ribbon-action-menu {
-        vertical-align: text-top;
-        margin: 2px 0 0 2px;
-        &.is-open {
-            z-index: 32;
-        }
-    }
-}
-
-.cw-ribbon-tools {
-    background-color: var(--white);
-    border: solid thin var(--content-color-40);
-    box-shadow: 2px 2px var(--dark-gray-color-30);
-    position: absolute;
-    right: -570px;
-    top: 15px;
-    height: 100%;
-    max-width: calc(100% - 28px);
-    display: flex;
-    flex-flow: row;
-    transition: right 0.8s;
-    z-index: 42;
-
-    &.unfold {
-        right: 0px;
-        margin-right: 15px;
-    }
-
-    &.cw-ribbon-tools-consume {
-        position: fixed;
-        &.unfold {
-            right: 15px;
-        }
-    }
-
-    &.cw-ribbon-tools-sticky {
-        position: absolute;
-        top: -1px;
-        margin-right: 0;
-    }
-
-    .cw-ribbon-tool-content {
-        height: 100%;
-        width: 540px;
-        background-color: var(--white);
-        padding: 0;
-        overflow: hidden;
-
-
-        .cw-ribbon-tool-content-nav {
-            position: sticky;
-            height: 100%;
-            top: 0;
-            background-color: var(--white);
-            margin: 0;
-            padding: 0;
-            color: var(--base-color);
-            display: flex;
-            z-index: 43;
-
-            .cw-tools-hide-button {
-                position: absolute;
-                border: none;
-                height: 36px;
-                width: 24px;
-                min-width: 24px;
-                margin-right: 1em;
-                padding: 0 4px;
-                right: 0;
-                top: 12px;
-                cursor: pointer;
-
-                @include background-icon(decline, clickable, 24);
-                background-repeat: no-repeat;
-                background-size: 24px;
-                background-position: center right;
-                background-color: var(--white);
-            }
-
-            >.cw-ribbon-tool-content-tablist {
-                width: 100%;
-                height: 100%;
-                >.cw-tabs-nav {
-                    border: none;
-                    width: calc(100% - 48px);
-
-                    >button {
-                        padding: 18px 8px 4px 8px;
-                        margin-top: 2px;
-                        max-width: unset;
-                        flex-grow: 0.5;
-                        &::after {
-                            margin-top: 16px;
-                        }
-                    }
-
-                }
-                >.cw-tabs-content {
-                    border: none;
-                    border-top: solid thin var(--content-color-40);
-                    padding: 0;
-                    height: calc(100% - 58px);
-
-                    >.cw-tab {
-                        height: calc(100% - 22px);
-                        padding: 14px 8px 8px 8px;
-                        position: relative;
-                        overflow-y: auto;
-                        overflow-x: hidden;
-                        scrollbar-width: thin;
-                        scrollbar-color: var(--base-color) var(--white);
-
-                        &.cw-ribbon-tool-blockadder-tab {
-                            height: 100%;
-                            overflow: hidden;
-                            padding: 0;
-                        }
-                    }
-                }
-            }
-        }
-
-        .cw-ribbon-tool {
-            padding: 14px 8px 6px 8px;
-            height: calc(100% - 64px);
-            overflow-y: auto;
-            overflow-x: hidden;
-            scrollbar-width: thin;
-            scrollbar-color: var(--base-color) var(--white);
-        }
-    }
-}
-
-#courseware-public-index {
-    .cw-ribbon-tools {
-        top: 127px;
-
-        &.cw-ribbon-tools-consume {
-            top: 14px;
-        }
-
-        &.cw-ribbon-tools-sticky {
-            top: 56px;
-        }
-    }
-}
-
-.cw-structural-element-consumemode {
-    .cw-ribbon-tools {
-        top: 25px;
-    }
-}
-
-.responsive-display {
-    .cw-ribbon-sticky-top,
-    .cw-ribbon-sticky-bottom,
-    .cw-ribbon-wrapper-consume,
-    .cw-ribbon-consume-bottom {
-        width: 100%;
-    }
-    .cw-ribbon {
-        &.cw-ribbon-sticky {
-            width: calc(100% - 62px);
-        }
-    }
-    .cw-ribbon-sticky-spacer {
-        height: 75px;
-    }
-}
-
-/* * * * * *
-ribbon end
-* * * * * */
-
-
-/* * * * * * * * *
- structual element
-* * * * * * * * * */
-
-.cw-structural-element {
-
-    &.cw-structural-element-consumemode {
-        position: fixed;
-        top: 0;
-        left: 0;
-        height: 100%;
-        width: 100%;
-        padding: 0;
-        background-color: var(--white);
-        z-index: 1004;
-        overflow-y: scroll;
-        overflow-x: hidden;
-        scrollbar-width: thin;
-        scrollbar-color: var(--base-color) var(--dark-gray-color-5);
-    }
-
-    .cw-welcome-screen {
-        .cw-welcome-screen-keyvisual {
-            margin: 14px 0 42px 0;
-            width: 100%;
-            height: 400px;
-            background-image: url('#{$image-path}/courseware-keyvisual.svg');
-            background-repeat: no-repeat;
-            background-position: center center;
-        }
-        header {
-            padding: 0.5em 0;
-            text-align: center;
-            font-size: 2.25em;
-        }
-        .cw-welcome-screen-actions {
-            display: flex;
-            flex-wrap: wrap;
-            justify-content: center;
-        }
-
-    }
-
-    .cw-structural-element-discussion {
-        max-width: 1606px;
-        width: 100%;
-        margin-bottom: 1em;
-    }
-
-    .cw-container-wrapper {
-        max-width: $max-content-width;
-        margin: 0;
-        padding: 0;
-        display: flex;
-        flex-wrap: wrap;
-        align-items: stretch;
-        justify-content: space-between;
-
-        &.cw-container-wrapper-consume {
-            margin: 0 auto;
-            padding: 91px 15px 15px 15px;
-        }
-
-        &.cw-container-wrapper-discuss {
-            max-width: 1606px;
-        }
-    }
-
-    .cw-structural-element-description {
-        width: 400px;
-        height: 200px;
-        overflow-y: auto;
-        resize: none;
-    }
-    .cw-structural-element-color {
-        color: var(--white);
-        &.black {
-            background-color: map-get($tile-colors, "black");
-        }
-        &.charcoal {
-            background-color: map-get($tile-colors, "charcoal");
-        }
-        &.royal-purple {
-            background-color: map-get($tile-colors, "royal-purple");
-        }
-        &.iguana-green {
-            background-color: map-get($tile-colors, "iguana-green");
-        }
-        &.queen-blue {
-            background-color: map-get($tile-colors, "queen-blue");
-        }
-        &.verdigris {
-            background-color: map-get($tile-colors, "verdigris");
-        }
-        &.mulberry {
-            background-color: map-get($tile-colors, "mulberry");
-        }
-        &.pumpkin {
-            background-color: map-get($tile-colors, "pumpkin");
-        }
-        &.sunglow {
-            background-color: map-get($tile-colors, "sunglow");
-        }
-        &.apple-green {
-            background-color: map-get($tile-colors, "apple-green");
-        }
-
-        &.studip-blue {
-            background-color: map-get($tile-colors, "studip-blue");
-        }
-        &.studip-lightblue {
-            background-color: map-get($tile-colors, "studip-lightblue");
-        }
-        &.studip-red {
-            background-color: map-get($tile-colors, "studip-red");
-        }
-        &.studip-green {
-            background-color: map-get($tile-colors, "studip-green");
-        }
-        &.studip-yellow {
-            background-color: map-get($tile-colors, "studip-yellow");
-        }
-        &.studip-gray {
-            background-color: map-get($tile-colors, "studip-gray");
-        }
-    }
-
-    .cw-structural-element-info {
-        width: 600px;
-        tr:first-child {
-            width: 12em;
-            vertical-align: top;
-        }
-    }
-}
-
-.cw-structural-element-dialog {
-    input[type=text] {
-        width: 20em;
-    }
-}
-
-.cw-structural-element-image-preview {
-    display: block;
-    max-height: 200px;
-    max-width: 400px;
-    width: auto;
-    height: auto;
-    margin: 0 auto;
-    padding-bottom: 1em;
-}
-
-.cw-structural-element-image-preview-placeholder {
-    width: 356px;
-    height: 200px;
-    margin: 0 auto;
-    background-color: var(--content-color-20);
-    background-size: 100% auto;
-    background-repeat: no-repeat;
-    background-position: center;
-    @include background-icon(courseware, clickable, 128);
-    margin-bottom: 1em;
-}
-
-.cw-element-permissions {
-    label {
-        display: block;
-        padding: 6px 0;
-    }
-    table.default {
-        > caption {
-            padding: 0;
-            font-size: 1.25em;
-        }
-        td.perm {
-            input.right, input.date {
-                cursor: pointer !important;
-            }
-        }
-    }
-    button.cw-add-persons {
-        margin-left: 4px;
-    }
-    button.cw-permission-delete {
-        width: 24px;
-        height: 24px;
-        border: none;
-        background-color: transparent;
-        @include background-icon(trash, clickable);
-        background-repeat: no-repeat;
-        cursor: pointer;
-    }
-}
-
-.cw-element-export {
-    label {
-        input {
-            vertical-align: middle;
-        }
-
-        span {
-            vertical-align: middle;
-        }
-    }
-}
-
-
-
-/* * * * * * * * * * * *
- structual element end
-* * * * * * * * * * * */
-
-/* * * * *
- container
-* * * * */
-.cw-container {
-    margin-bottom: 1em;
-
-    &.cw-container-colspan-full {
-        max-width: $max-content-width;
-        width: 100%;
-    }
-    &.cw-container-colspan-half {
-        max-width: 540px;
-        width: 100%;
-    }
-    &.cw-container-colspan-half-center {
-        width: $max-content-width;
-        .cw-container-content {
-            max-width: 540px;
-            margin: auto;
-        }
-    }
-
-    .cw-container-header {
-        background-color: var(--content-color-20);
-        padding: 4px 10px 4px 22px;
-
-        .cw-container-header-toggle {
-            display: inline-block;
-            width: calc(100% - 40px);
-        }
-
-        span {
-            color: var(--base-color);
-            font-weight: 700;
-            line-height: 2em;
-            font-size: 1.1em;
-
-            &.cw-default-container-blocker-warning {
-                font-weight: 400;
-            }
-        }
-
-        img {
-            vertical-align: text-bottom;
-        }
-
-        .cw-container-actions {
-            position: relative;
-            float: right;
-            margin-top: 4px;
-            // z-index: 31;
-            .is-open {
-                z-index: 31;
-            }
-        }
-    }
-
-    &.cw-container-active {
-        &.cw-container-colspan-half-center {
-            .cw-container-content {
-                max-width: unset;
-                .cw-block-wrapper {
-                    max-width: 540px;
-                    margin: auto;
-                }
-            }
-        }
-        .cw-container-content {
-            border: solid thin var(--content-color-40);
-        }
-    }
-
-
-    .cw-block-wrapper {
-        padding: 0;
-        margin: 0;
-        list-style: none;
-
-        &.cw-block-wrapper-active {
-            padding: 14px 10px;
-        }
-
-        .cw-block-item {
-            padding: 0;
-            margin: 0 0 1em 0;
-        }
-    }
-
-    .cw-container-list-block-list {
-        padding: 0;
-        list-style: none;
-    }
-
-    .cw-container-tabs-block-list {
-        list-style: none;
-        padding: 1em 1em 0 1em;
-    }
-
-    .cw-container-accordion-block-list {
-        list-style: none;
-        padding: 0 1em;
-    }
-}
-
-.cw-container-section-delete {
-    img {
-        cursor: pointer;
-    }
-}
-
-form.cw-container-dialog-edit-form {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 8px;
-
-    fieldset {
-        max-width: 200px;
-    }
-}
-
-.cw-container-wrapper-discuss {
-    flex-direction: column;
-
-    .cw-container-colspan-full {
-        max-width: unset;
-    }
-    .cw-container-colspan-half-center,
-    .cw-container-colspan-half {
-        max-width: 1050px;
-    }
-    .cw-container-colspan-half-center {
-        width: 100%;
-        .cw-container-content {
-            width: 1050px;
-        }
-    }
-}
-
-/* * * * * * *
- container end
-* * * * * * */
-
-/* * *
- block
-* * */
-.cw-default-block {
-    display: flex;
-    flex-flow: row;
-    .cw-default-block-invisible-info {
-        img {
-            vertical-align: text-bottom;
-        }
-    }
-
-}
-.cw-content-wrapper {
-    display: flex;
-    flex-flow: column;
-    width: 100%;
-    .cw-block-content {
-        overflow: auto;
-        position: relative;
-    }
-}
-.cw-content-wrapper-active {
-    border: solid thin var(--content-color-40);
-    .cw-block-content {
-        padding: $cw-wrapper-gap;
-    }
-}
-.cw-container-wrapper-discuss {
-    .cw-container-colspan-full {
-        .cw-content-wrapper {
-            max-width: $max-content-width;
-        }
-    }
-    .cw-container-colspan-half,
-    .cw-container-colspan-half-center {
-        .cw-content-wrapper {
-            max-width: 540px;
-        }
-    }
-}
-.cw-block-header {
-    background-color: var(--content-color-20);
-    padding: 4px 10px 4px 22px;
-
-    .cw-block-header-toggle {
-        display: inline-block;
-        width: calc(100% - 50px);
-    }
-
-    span {
-        color: var(--base-color);
-        font-weight: 700;
-        line-height: 2em;
-        font-size: 1.1em;
-
-        &.cw-default-block-invisible-info,
-        &.cw-default-block-blocker-warning {
-            font-weight: 400;
-        }
-    }
-
-    img {
-        vertical-align: text-bottom;
-    }
-
-    .cw-block-actions {
-        position: relative;
-        float: right;
-        margin-top: 4px;
-        .is-open{
-            z-index: 30;
-        }
-    }
-}
-
-.cw-discuss-wrapper,
-.cw-block-features {
-
-    header{
-        background-color: var(--content-color-20);
-        color: var(--base-color);
-        font-weight: 600;
-        padding: 0.5em;
-    }
-
-    .cw-block-features-content{
-        margin: 1em;
-    }
-}
-
-.cw-discuss-wrapper {
-    flex-shrink: 3;
-    flex-grow: 2;
-    margin-left: 10px;
-}
-
-.cw-draggable-shapes-wrapper {
-    position: absolute;
-    top: 0px;
-    left: 0px;
-    width: 100%;
-    height: 100%;
-    margin: $cw-wrapper-gap;
-
-    .cw-draggable-area {
-        width: 100%;
-        height: 100%;
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        text-align: center;
-        &:hover {
-            cursor: grab;
-        }
-    }
-}
-
-@media only screen and (max-width: 1820px) {
-    .cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss {
-        max-width: $max-content-width;
-        .cw-container.cw-container-list.cw-container-item.cw-container-colspan-full {
-            .cw-default-block {
-                flex-flow: column;
-                .cw-discuss-wrapper {
-                    margin-left: 0;
-                    margin-top: 8px;
-                }
-            }
-        }
-    }
-}
-
-@media only screen and (max-width: 1200px) {
-    .cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss {
-        max-width: $max-content-width;
-        .cw-container.cw-container-list.cw-container-item.cw-container-colspan-half,
-        .cw-container.cw-container-list.cw-container-item.cw-container-colspan-half-center {
-            .cw-default-block {
-                flex-flow: column;
-                .cw-discuss-wrapper {
-                    margin-left: 0;
-                    margin-top: 8px;
-                    max-width: 540px;
-                }
-            }
-        }
-    }
-}
-
-.cw-button-feature-close {
-    float: right;
-    border: none;
-    height: 24px;
-    width: 24px;
-    @include background-icon(decline, clickable);
-    background-repeat: no-repeat;
-    background-color: transparent;
-    cursor: pointer;
-}
-
-.cw-keypoint-content {
-    display: flex;
-    min-height: 52px;
-    padding:1.5em 2.5em 1.5em 2.5em;
-    border-style: solid;
-    border-width: 2px;
-    align-items: center;
-}
-.cw-keypoint-content > img {
-    flex-shrink: 0;
-}
-.cw-keypoint-red {
-    border-color: map-get($icon-colors, 'icon-red');
-}
-.cw-keypoint-blue {
-    border-color: map-get($icon-colors, 'icon-blue');
-}
-.cw-keypoint-green {
-    border-color: map-get($icon-colors, 'icon-green');
-}
-.cw-keypoint-gray {
-    border-color: map-get($icon-colors, 'icon-gray');
-}
-.cw-keypoint-yellow {
-    border-color: map-get($icon-colors, 'icon-yellow');
-}
-.cw-keypoint-sentence {
-    display: inline;
-    font-size: 1.25em;
-    padding-left: 1.5em;
-    margin-top: 10px;
-}
-.cw-keypoint-label-color {
-    width: 32px !important;
-    min-width: 32px !important;
-    height: 32px;
-    padding: 5px !important;
-
-    > .cw-keypoint-input-color {
-        visibility: hidden;
-        position: absolute;
-    }
-
-    > .cw-keypoint-input-color + div {
-        cursor: pointer;
-        border: 2px solid transparent;
-        height: 32px;
-        width: 32px;
-    }
-
-    > .cw-keypoint-input-color:checked + div { /* (RADIO CHECKED) IMAGE STYLES */
-        @include background-icon(accept, info_alt, 24);
-        background-repeat: no-repeat;
-        background-position: center;
-    }
-}
-label[for="cw-keypoint-color"] {
-    vertical-align: top;
-}
-.cw-keypoint-label-color {
-    display: inline-block !important;
-}
-.cw-keypoint-icons {
-    background-color: var(--white);
-}
-.cw-keypoint-bg-red {
-    background-color: map-get($icon-colors, 'icon-red');
-}
-.cw-keypoint-bg-blue {
-    background-color: map-get($icon-colors, 'icon-blue');
-}
-.cw-keypoint-bg-green {
-    background-color: map-get($icon-colors, 'icon-green');
-}
-.cw-keypoint-bg-gray {
-    background-color: map-get($icon-colors, 'icon-gray');
-}
-.cw-keypoint-bg-yellow {
-    background-color: map-get($icon-colors, 'icon-yellow');;
-}
-
-
-.cw-static-content-iframe {
-    width: 100%;
-}
-.cw-static-content-preview {
-    span {
-        display: block;
-    }
-    img {
-        width: 50%;
-    }
-}
-
-.cw-block-edit textarea {
-    width: -moz-available;
-    height: -moz-available;
-    min-height: 8em;
-    border: solid thin var(--content-color-40);
-    resize: none;
-}
-
-.cw-typewriter-content {
-
-    .vue-typer {
-
-        &.font-typewriter{
-            font-family: "Lucida Sans Typewriter", "Lucida Console", Monaco, "Bitstream Vera Sans Mono", monospace;
-        }
-        &.font-trebuchet {
-            font-family: "Trebuchet MS", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
-        }
-        &.font-tahoma {
-            font-family: Tahoma, Verdana, Segoe, sans-serif;
-        }
-        &.font-georgia {
-            font-family: Georgia, Times, "Times New Roman", serif;
-        }
-        &.font-narrow {
-            font-family: "Arial Narrow", Arial, "Helvetica Condensed", Helvetica, sans-serif;
-        }
-
-        &.size-tall {
-            font-size: 1.25em;
-            line-height: 1.25em;
-        }
-        &.size-grande {
-            font-size: 1.5em;
-            line-height: 1.25em;
-        }
-        &.size-huge {
-            font-size: 2em;
-            line-height: 1.25em;
-        }
-    }
-}
-
-.cw-block-actions {
-    padding-left: 14px;
-}
-.cw-button-box {
-    float: right;
-}
-
-/* * * * * *
- block end
-* * * * * */
-
-/* * * * * * * *
- sortable handle
- * * * * * * * */
-
-.cw-sortable-handle {
-    @extend .drag-handle;
-    display: inline-block;
-    height: 24px;
-    &.cw-sortable-handle-dragging {
-        cursor: grabbing;
-    }
-}
-
-.cw-block-item-sortable {
-    .cw-sortable-handle {
-        position: relative;
-        left: 4px;
-    }
-    .cw-block {
-        margin-top: -38px;
-    }
-}
-
-.cw-container-item-sortable {
-    .cw-sortable-handle {
-        position: relative;
-        left: 4px;
-    }
-    .cw-container {
-        margin-top: -38px;
-    }
-}
-
-.cw-collapsible-open {
-    .cw-container-list-sort-mode {
-        margin-top: 8px;
-    }
-}
-
-.container-ghost,
-.block-ghost {
-    opacity: 0.6;
-}
-
-
-.cw-container-wrapper-edit {
-    .cw-structural-element-list {
-        width: 100%;
-        padding: 0;
-        margin-top: 8px;
-        list-style: none;
-    }
-}
-
-.cw-block-item-selected {
-    .cw-block-header {
-        font-style: italic;
-    }
-}
-.cw-container-item-selected {
-    .cw-container-header {
-        font-style: italic;
-    }
-}
-
-/* * * * * * * * * * *
- sortable handle end
- * * * * * * * * * * */
-
-/* * * * *
- t r e e
- * * * * */
-
-.cw-tree {
-    ol {
-        list-style: none;
-        padding-left: 1.25em;
-        margin-bottom: 20px;
-
-        &.cw-tree-root-list {
-            padding-left: 0;
-            > li.cw-tree-item {
-                > .cw-tree-item-wrapper {
-                    border-bottom: solid thin var(--content-color-40);
-                    display: block;
-                    > a.cw-tree-item-link {
-                        display: block;
-                        font-size: 18px;
-                        padding-left: 26px;
-                        @include background-icon(courseware, clickable, 18);
-                        background-repeat: no-repeat;
-                        background-position: 3px 3px;
-                    }
-                }
-
-                ol {
-                    padding-left: 0;
-                    > li.cw-tree-item {
-                        margin: 28px 0 0 0;
-                        > .cw-tree-item-wrapper {
-                            display: block;
-                            border-bottom: solid thin var(--content-color-40);
-                            margin-bottom: 12px;
-                            > a.cw-tree-item-link {
-                                display: inline-block;
-                                width: calc(100% - 4px);
-                                padding-left: 4px;
-                                font-size: 16px;
-                                &.cw-tree-item-link-edit {
-                                    width: calc(100% - 20px);
-                                }
-                            }
-                        }
-                        ol {
-                            padding-left: 0.25em;
-                            &.cw-tree-draggable-list {
-                                padding-left: 1em;
-                            }
-                            > li.cw-tree-item {
-                                margin: 4px 0;
-                                > .cw-tree-item-wrapper {
-                                    border: none;
-                                    > a.cw-tree-item-link {
-                                        display: inline-block;
-                                        border-bottom: none;
-                                        font-size: 14px;
-                                        width: calc(100% - 20px);
-                                        background-repeat: no-repeat;
-                                        padding-left: 18px;
-                                        margin-left: 4px;
-                                        margin-bottom: 0;
-
-                                        &.cw-tree-item-link-edit {
-                                            width: calc(100% - 38px);
-                                        }
-
-                                        @include background-icon(bullet-dot, clickable, 18);
-                                        &:hover {
-                                            @include background-icon(bullet-dot, attention, 18);
-                                        }
-                                        &.cw-tree-item-link-current {
-                                            @include background-icon(bullet-dot, info, 18);
-                                        }
-                                    }
-                                }
-                                ol {
-                                    padding-left: 1em;
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    .cw-sortable-handle {
-        vertical-align: middle;
-    }
-    .cw-tree-item-link {
-        &:hover {
-            background-color: var(--light-gray-color-20);
-            color: var(--active-color);
-        }
-        &.cw-tree-item-link-current {
-            color: var(--black);
-            font-weight: 600;
-            cursor: default;
-            &::before {
-                color: var(--black);
-            }
-        }
-        &.cw-tree-item-link-selected {
-            font-style: italic;
-            font-weight: 600;
-        }
-
-
-        @each $type, $icon in $tree-item-flag-icons {
-            .cw-tree-item-flag-#{$type} {
-                display: inline-block;
-                width: 16px;
-                height: 16px;
-                vertical-align: top;
-                @include background-icon(#{$icon}, clickable, 16);
-            }
-            &:hover .cw-tree-item-flag-#{$type} {
-                @include background-icon(#{$icon}, attention, 16);
-            }
-            &.cw-tree-item-link-current .cw-tree-item-flag-#{$type} {
-                @include background-icon(#{$icon}, info, 16);
-            }
-        }
-    }
-    .cw-tree-item-sequential {
-        display: inline-block;
-        position: absolute;
-        right: 8px;
-
-        &.cw-tree-item-sequential-complete {
-            width: 16px;
-            height: 16px;
-            vertical-align: top;
-            @include background-icon(accept, info, 16);
-        }
-        &.cw-tree-item-sequential-percentage {
-            color: var(--black);
-            font-size: 14px;
-        }
-    }
-
-    .cw-tree-item-ghost {
-        opacity: 0.6;
-    }
-}
-
-/* * * * * * *
-t r e e  e n d
-* * * * * * * */
-
-/* * * * * * * * * * * * * * *
-c o l l a p s i b l e   b o x
-* * * * * * * * * * * * * * */
-.cw-collapsible {
-
-    border: solid thin var(--content-color-40);
-    margin-bottom: -1px;
-
-    .cw-collapsible-title {
-        @include background-icon(arr_1right, clickable);
-        background-position: 6px center;
-        background-repeat: no-repeat;
-        background-color: var(--content-color-20);
-        padding: 0.5em 2em;
-        margin: 0;
-        font-weight: 600;
-        color: var(--base-color);
-        cursor: pointer;
-
-        &.cw-collapsible-open {
-            @include background-icon(arr_1down, clickable);
-        }
-
-        img {
-            vertical-align: top;
-        }
-    }
-
-    .cw-collapsible-content {
-        display: none;
-        &.cw-collapsible-content-open {
-            display: block;
-            padding: 10px;
-        }
-    }
-}
-
-form .cw-collapsible .cw-collapsible-content.cw-collapsible-content-open {
-    padding: unset;
-    label {
-        margin: 1.5ex;
-    }
-}
-
-/* * * * * * * * * * * * * * * * * *
-c o l l a p s i b l e  b o x  e n d
-* * * * * * * * * * * * * * * * * */
-
-/* * * *
-t a b s
-* * * */
-$icons: (
-    accept,
-    add,
-    add-circle,
-    admin,
-    aladdin,
-    arr_1down,
-    arr_1left,
-    arr_1right,
-    arr_1up,
-    arr_2down,
-    arr_2left,
-    arr_2right,
-    arr_2up,
-    audio,
-    audio3,
-    billboard,
-    block-canvas,
-    block-comparison,
-    block-eyecatcher,
-    block-gallery,
-    block-gallery2,
-    block-imagemap,
-    brainstorm,
-    campusnavi,
-    chat2,
-    code,
-    community2,
-    computer,
-    consultation,
-    content,
-    courseware,
-    crown,
-    date-single,
-    decline,
-    decline-circle,
-    doctoral_cap,
-    download,
-    dropbox,
-    edit,
-    exclaim,
-    exclaim-circle,
-    export,
-    favorite,
-    filter,
-    globe,
-    graph,
-    group2,
-    group3,
-    group4,
-    home2,
-    info,
-    info-circle,
-    install,
-    institute,
-    key,
-    knife,
-    learnmodule,
-    lightbulb,
-    lightbulb2,
-    link2,
-    link3,
-    link-extern,
-    link-intern,
-    literature,
-    lock-locked,
-    lock-unlocked,
-    mail2,
-    medal,
-    metro,
-    microphone,
-    module,
-    network,
-    notification,
-    notification2,
-    opencast,
-    outer-space,
-    permalink,
-    person,
-    phone,
-    picture,
-    place,
-    plugin,
-    question,
-    question-circle,
-    ranking,
-    remove,
-    remove-circle,
-    resources,
-    roles,
-    schedule2,
-    search,
-    settings,
-    span-empty,
-    span-1quarter,
-    span-2quarter,
-    span-3quarter,
-    span-full,
-    spiral,
-    sport,
-    staple,
-    star,
-    star-empty,
-    star-halffull,
-    test,
-    tools,
-    topic,
-    ufo,
-    video2,
-    visibility-visible,
-    wizard
-);
-
-.cw-tabs-nav {
-    display: flex;
-    flex-wrap: wrap;
-    list-style: none;
-    padding: 0;
-    margin: 0;
-    border: solid thin var(--content-color-40);
-    border-bottom: none;
-
-    button {
-        background-color: var(--white);
-        border: none;
-        padding: 1em 0 4px 0;
-        margin: 0 7px 0 21px;
-        color: var(--base-color);
-        cursor: pointer;
-        text-align: center;
-        flex-grow: 1;
-        max-width: max-content;
-
-        &::after {
-            display: block;
-            margin-top: 4px;
-            margin-bottom: -5px;
-            margin-left: -14px;
-            width: calc(100% + 28px);
-            content: '';
-            border-bottom: solid 3px var(--dark-gray-color-75);
-            transform: scaleX(0);
-            transition: transform var(--transition-duration) ease-in-out;
-        }
-
-        &.is-active,
-        &:hover {
-            color: var(--black);
-            &:after {
-                transform: scaleX(1);
-            }
-        }
-
-        @each $icon in $icons {
-            &.cw-tabs-nav-icon-text-#{$icon} {
-                &::before {
-                    @include background-icon($icon);
-                    background-repeat: no-repeat;
-                    background-position: left bottom;
-
-                    display: inline-block;
-                    height: 16px;
-                    width: 16px;
-                    margin-bottom: -2px;
-                    padding-left: 4px;
-                    content: '';
-                }
-
-            }
-            &.is-active.cw-tabs-nav-icon-text-#{$icon},
-            &.cw-tabs-nav-icon-text-#{$icon}:hover {
-                &::before {
-                    @include background-icon($icon, info);
-                }
-            }
-        };
-        @each $icon in $icons {
-            &.cw-tabs-nav-icon-solo-#{$icon} {
-                &::before {
-                    display: inline-block;
-                    height: 24px;
-                    width: 24px;
-                    content: '';
-
-                    @include background-icon($icon, clickable, 24);
-                    background-repeat: no-repeat;
-                    background-position: center;
-                }
-            }
-            &.is-active.cw-tabs-nav-icon-solo-#{$icon},
-            &.cw-tabs-nav-icon-solo-#{$icon}:hover {
-                &::before {
-                    @include background-icon($icon, info);
-                }
-            }
-        };
-
-    }
-
-    &:hover li {
-        &.is-active::after {
-            transform: scaleX(0);
-        }
-        &:hover::after {
-            transform: scaleX(1);
-        }
-    }
-}
-.cw-tabs-content {
-    border: solid thin var(--content-color-40);
-    padding: 4px;
-}
-.cw-block-wrapper-active {
-    .cw-tabs-content {
-        padding: 14px 0;
-    }
-}
-
-.studip-dialog-with-tab {
-    .studip-dialog-body .studip-dialog-content {
-        padding: 0 4px;
-        .cw-tab-in-dialog {
-            .cw-tabs-nav {
-                border: none;
-                border-bottom: solid thin var(--content-color-40);
-                margin-bottom: 4px;
-            }
-            .cw-tabs-content {
-                border: none;
-                min-width: 500px;
-                min-height: 400px;
-                overflow-y: auto;
-                scrollbar-width: thin;
-                scrollbar-color: var(--base-color) var(--dark-gray-color-5);
-            }
-        }
-    }
-}
-
-.cw-tabs {
-    .cw-tab {
-        display: none;
-        height: 0;
-
-        &.cw-tab-active {
-            display: block;
-            height: unset;
-            padding: 4px 8px;
-        }
-    }
-    &.cw-course-manager-tabs {
-        .cw-tab {
-            display: none;
-
-            &.cw-tab-active {
-                display: block;
-            }
-        }
-    }
-}
-
-/* * * * * * * *
-t a b s  e n d
-* * * * * * * */
-
-/* * * * * * * * * *
-b l o c k a d d e r
-* * * * * * * * * */
-
-.cw-tools-element-adder-tabs {
-    .cw-tabs-nav {
-        margin-top: 4px;
-        border: none;
-        border-bottom: solid thin var(--content-color-40);
-
-        button {
-            max-width: unset;
-            padding: 1em 1.5em 4px 1.5em;
-            margin: 0px 2em;
-        }
-    }
-    .cw-tabs-content {
-        border: none;
-        overflow-x: hidden;
-        overflow-y: auto;
-        scrollbar-width: thin;
-        scrollbar-color: var(--base-color) var(--dark-gray-color-5);
-
-        .input-group.files-search {
-            &.search {
-                border: thin solid var(--dark-gray-color-30);
-                margin-bottom: 0px;
-                input {
-                    border: none;
-                }
-            }
-
-            .input-group-append {
-                .button {
-                    border: none;
-                    border-left: thin solid var(--dark-gray-color-30);
-                    &.active {
-                        background-color: var(--base-color);
-                    }
-                }
-                .reset-search {
-                    border: none;
-                    background-color: var(--white);
-                }
-            }
-
-            .active-filter {
-                display: flex;
-                align-items: center;
-                justify-content: space-between;
-                border: solid thin var(--black);
-                background-color: var(--content-color-10);
-                margin: 3px;
-                padding: 2px 3px;
-
-                .removefilter {
-                    border: none;
-                    background-color: transparent;
-                }
-            }
-        }
-
-        .cw-block-search {
-            width: inherit;
-        }
-
-        .filterpanel {
-            margin-bottom: 5px;
-            padding: 2px;
-            border: thin solid var(--dark-gray-color-30);
-            border-top: none;
-            background-color: var(--white);
-
-            .button {
-                min-width: inherit;
-                margin: 4px 2px;
-
-                &.button-active {
-                    background-color: var(--base-color);
-                    color: var(--white);
-                }
-            }
-        }
-    }
-
-    .cw-collapsible {
-        .cw-collapsible-content {
-            display: none;
-            &.cw-collapsible-content-open {
-                display: block;
-            }
-        }
-    }
-}
-
-.cw-element-adder-wrapper {
-    display: flex;
-    flex-wrap: wrap;
-    justify-content: space-between;
-}
-.cw-blockadder-item-list {
-    display: grid;
-    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
-    grid-auto-rows: auto;
-    grid-gap: 4px;
-
-    .cw-blockadder-item-wrapper {
-        display: flex;
-        border: solid thin var(--content-color-40);
-        max-width: 254px;
-
-        &:hover {
-            border-color: var(--base-color);
-        }
-        .cw-blockadder-item {
-            padding: 64px 10px 4px 10px;
-            @include background-icon(unit-test, clickable, 48);
-            background-position: 10px 10px;
-            background-repeat: no-repeat;
-            cursor: pointer;
-
-            @each $item, $icon in $blockadder-items {
-                &.cw-blockadder-item-#{$item} {
-                    @include background-icon($icon, clickable, 48);
-                }
-            }
-            .cw-clipboard-item-title,
-            .cw-blockadder-item-title {
-                display: inline-block;
-                font-weight: 600;
-                margin-bottom: 2px;
-            }
-            .cw-blockadder-item-description {
-                display: inline-block;
-                margin: 0 0 4px;
-            }
-        }
-        .cw-blockadder-item-fav {
-            height: 32px;
-            padding: 8px;
-            background-color: transparent;
-            border: none;
-            cursor: pointer;
-        }
-    }
-}
-
-.cw-block-adder-area {
-    background-color: var(--white);
-    border: solid thin var(--content-color-40);
-    padding: 1em 0;
-    color: var(--base-color);
-    text-align: center;
-    width: 100%;
-    font-weight: 600;
-    cursor: pointer;
-
-    &:hover {
-        border-color: var(--base-color);
-    }
-
-    &.cw-block-adder-active {
-        border: solid thin var(--base-color);
-        background-color: var(--base-color);
-        color: var(--white);
-    }
-
-    img {
-        vertical-align: text-bottom;
-    }
-}
-.cw-block-helper-buttons {
-    display: inline-block;
-    width: 100%;
-
-    .cw-block-helper-reset {
-        float: right;
-    }
-
-    .button.cw-block-helper-reset::before {
-        content: '';
-        @include background-icon(refresh);
-        background-repeat: no-repeat;
-        float: left;
-        height: 16px;
-        width: 16px;
-        margin: 1px 5px 0 -8px;
-    }
-}
-
-.cw-block-helper-results {
-    margin-top: 5px;
-}
-
-.cw-containeradder-item {
-    margin-bottom: 4px;
-    padding: 1em 1em 1em 6em;
-    @include background-icon(unit-test, clickable, 48);
-    background-position: 12px center;
-    background-repeat: no-repeat;
-    border: solid thin var(--content-color-40);
-    cursor: pointer;
-
-    &:hover {
-        border-color: var(--base-color);
-    }
-
-    @each $item, $icon in $containeradder-items {
-        &.cw-containeradder-item-#{$item} {
-            @include background-icon($icon, clickable, 48);
-        }
-    }
-
-    .cw-containeradder-item-title {
-        font-weight: 600;
-    }
-}
-
-.cw-container-style-selector {
-    display: flex;
-    margin-bottom: 8px;
-
-    label {
-        border: solid thin var(--content-color-40);
-        padding: calc(0.5em + 32px) 1em 0.5em 1em;
-        color: var(--base-color);
-        text-align: center;
-        width: 33%;
-        background-position: center 0.5em;
-        background-repeat: no-repeat;
-        cursor: pointer;
-
-        &.full {
-            @include background-icon(column-full, clickable, 32);
-        }
-        &.half {
-            @include background-icon(column-half, clickable, 32);
-        }
-        &.half-center {
-            @include background-icon(column-half-centered, clickable, 32);
-        }
-        &:hover {
-            color: var(--active-color);
-        }
-        &:not(:first-child) {
-            border-left: solid thin transparent;
-        }
-        &.cw-container-style-selector-active {
-            background-color: var(--content-color-20);
-            border: solid thin var(--base-color);
-        }
-
-    }
-    input[type=radio] {
-        position: absolute;
-        opacity: 0;
-        width: 0;
-
-        &:focus + label {
-            outline-color: Highlight;
-            outline-color: -webkit-focus-ring-color;
-            outline-style: auto;
-            outline-width: 1px;
-        }
-    }
-}
-.cw-element-inserter-wrapper {
-    display: flex;
-    flex-wrap: wrap;
-    justify-content: space-between;
-}
-
-
-.cw-clipboard-item-wrapper {
-    display: flex;
-    width: calc(50% - 4px);
-    border: solid thin var(--content-color-40);
-    margin-bottom: 4px;
-
-    &:hover {
-        border-color: var(--base-color);
-    }
-
-    .cw-clipboard-item {
-        width: 207px;
-        padding: 64px 10px 4px 10px;
-        @include background-icon(unit-test, clickable, 48);
-        background-position: 10px 10px;
-        background-repeat: no-repeat;
-        cursor: pointer;
-        background-color: var(--white);
-        border: none;
-        text-align: left;
-        color: var(--base-color);
-
-        @each $item, $icon in $blockadder-items {
-            &.cw-clipboard-item-#{$item} {
-                @include background-icon($icon, clickable, 48);
-            }
-        }
-        @each $item, $icon in $containeradder-items {
-            &.cw-clipboard-item-#{$item} {
-                @include background-icon($icon, clickable, 48);
-            }
-        }
-    
-        .cw-clipboard-item-title {
-            display: inline-block;
-            font-weight: 600;
-            margin-bottom: 2px;
-        }
-
-    }
-    .cw-clipboard-item-action-menu-wrapper {
-        padding: 8px;
-    }
-}
-.action-menu.is-open,
-.action-menu-wrapper.is-open {
-    &.cw-clipboard-item-action-menu {
-        z-index: 42;
-    }
-}
-
-/* * * * * * * * * * * * *
-b l o c k a d d e r  e n d
-* * * * * * * * * * * * */
-
-/* * * * * * * * * * * * * * * *
-c o m p a n i o n  o v e r l a y
-* * * * * * * * * * * * * * * * */
-
-.cw-companion-overlay {
-    position: fixed;
-    bottom: 46px;
-    right: 0;
-    width: 360px;
-    max-width: calc(100% - 140px);
-    height: 120px;
-    z-index: 42000;
-    border: solid thin var(--content-color-40);
-    background-color: var(--white);
-    background-repeat: no-repeat;
-    background-position: 1em center;
-    background-size: 100px;
-    box-shadow: 5px 5px var(--dark-gray-color-10);
-    padding-left: 120px;
-    transform: translateX(100%);
-    transition: transform .5s ease;
-
-    @each $type, $image in $companion-types {
-        &.#{$type} {
-            background-image: url("#{$image-path}/companion/Tin_#{$image}.svg");
-        }
-    }
-
-    &.cw-companion-overlay-in {
-        transform: translateX(0);
-        right: 12px;
-    }
-
-    .cw-companion-overlay-content {
-        display: inline-block;
-        position: relative;
-        top: 25%;
-        padding: 0 1em;
-    }
-
-    .cw-compantion-overlay-close {
-        @include background-icon(decline);
-        background-color: var(--white);
-        background-repeat: no-repeat;
-
-        position: absolute;
-        top: 7px;
-        right: 7px;
-        height: 16px;
-        width: 16px;
-        border: none;
-        cursor: pointer;
-    }
-}
-
-.cw-companion-box {
-    display: flex;
-    height: 120px;
-    border: solid thin var(--content-color-40);
-    background-color: var(--white);
-    background-repeat: no-repeat;
-    background-position: 1em center;
-    background-size: 100px;
-    padding-left: 120px;
-    align-items: center;
-    margin-bottom: 1em;
-
-    @each $type, $image in $companion-types {
-        &.#{$type} {
-            background-image: url("#{$image-path}/companion/Tin_#{$image}.svg");
-        }
-    }
-
-    &.cw-companion-box-in-form {
-        margin-top: 8px;
-    }
-
-    p {
-        margin: 0 1em 10px 0;
-    }
-}
-
-.cw-companion-box-wrapper {
-        width: 100%;
-        max-width: $max-content-width;
-}
-
-/* * * * * * * * * * * * * * * * * *
-c o m p a n i o n  s m a l l  e n d
-* * * * * * * * * * * * * * * * * */
-
-/* * * * * * * * * *
-v i e w  w i d g e t
-* * * * * * * * * * */
-.cw-action-widget {
-    .cw-action-widget-show-toc {
-        @include background-icon(table-of-contents, clickable);
-    }
-    .cw-action-widget-edit{
-        @include background-icon(edit, clickable);
-    }
-    .cw-action-widget-sort{
-        @include background-icon(arr_1sort, clickable);
-    }
-    .cw-action-widget-add{
-        @include background-icon(add, clickable);
-    }
-    .cw-action-widget-info{
-        @include background-icon(info, clickable);
-    }
-    .cw-action-widget-star{
-        @include background-icon(star, clickable);
-    }
-    .cw-action-widget-trash{
-        @include background-icon(trash, clickable);
-    }
-    .cw-action-widget-oer{
-        @include background-icon(oer-campus, clickable);
-    }
-    .cw-action-widget-remove-lock{
-        @include background-icon(lock-unlocked, clickable);
-    }
-}
-.cw-export-widget {
-    .cw-export-widget-export{
-        @include background-icon(export, clickable);
-    }
-    .cw-export-widget-export-pdf{
-        @include background-icon(export, clickable);
-    }
-    .cw-export-widget-oer{
-        @include background-icon(share, clickable);
-    }
-}
-.cw-import-widget {
-    .cw-import-widget-archive{
-        @include background-icon(import, clickable);
-    }
-    .cw-import-widget-copy{
-        @include background-icon(copy, clickable);
-    }
-    .cw-import-widget-import{
-        @include background-icon(import, clickable);
-    }
-    .cw-action-widget-link {
-        @include background-icon(group, clickable);
-    }
-}
-
-/* * * * * * * * * * * * * *
-v i e w  w i d g e t  e n d
-* * * * * * * * * * * * * */
-
-/* * * * * * * * * * * * * * * * * *
-c o m m e n t s  &  f e e d b a c k
-* * * * * * * * * * * * * * * * * * */
-
-.cw-structural-element-feedback,
-.cw-structural-element-comments {
-    padding: 0 1em;
-}
-
-.cw-structural-element-feedback-items,
-.cw-structural-element-comments-items,
-.cw-block-feedback-items,
-.cw-block-comments-items {
-    min-height: 1em;
-    max-height: 225px;
-    overflow-y: auto;
-    overflow-x: hidden;
-    margin: -1em -1em 0em 0em;
-    padding: 0em  1em 0em 0em;
-    scroll-behavior: smooth;
-}
-
-.studip-dialog-content {
-    .cw-structural-element-feedback-items,
-    .cw-structural-element-comments-items,
-    .cw-block-feedback-items,
-    .cw-block-comments-items {
-        max-height: unset;
-    }
-}
-
-.cw-talk-bubble {
-    margin: 10px 20px;
-    position: relative;
-    width: 85%;
-    height: auto;
-    background-color: var(--dark-gray-color-10);
-    float: left;
-
-    .cw-talk-bubble-talktext {
-        padding: 1em;
-        text-align: left;
-        line-height: 1.5em;
-
-        .cw-talk-bubble-talktext-time {
-            color: var(--dark-gray-color-80);
-            text-align: right;
-            font-size: 0.8em;
-            margin-bottom: -0.5em;
-        }
-    }
-
-    &.cw-talk-bubble-own-post {
-        float: right;
-    }
-
-    &:after{
-        content: ' ';
-        position: absolute;
-        width: 0;
-        height: 0;
-        top: 0px;
-        bottom: auto;
-        border: 22px solid;
-        border-color: var(--dark-gray-color-10) transparent transparent transparent;
-        left: -20px;
-        right: auto;
-    }
-
-    &.cw-talk-bubble-own-post:after{
-        left: auto;
-        right: -20px;
-    }
-
-    .cw-talk-bubble-user {
-        padding: 1em 1em 0 1em;
-
-        .cw-talk-bubble-avatar{
-            display: inline-block;
-        }
-
-        span {
-            padding-left: 4px;
-            color: var(--dark-gray-color-45);
-            font-weight: 600;
-            vertical-align: top;
-        }
-    }
-}
-
-.cw-structural-element-feedback-create,
-.cw-structural-element-comment-create,
-.cw-block-feedback-create,
-.cw-block-comment-create {
-    border-top: solid thin var(--content-color-40);
-    padding-top: 1em;
-    textarea {
-        width: calc(100% - 6px);
-        resize: none;
-        border: solid thin var(--content-color-40);
-        &:active {
-            border: solid thin var(--content-color-80);
-        }
-    }
-}
-.cw-structural-element-comments-empty,
-.cw-structural-element-feedback-empty,
-.cw-block-comments-empty,
-.cw-block-feedback-empty {
-    .cw-structural-element-feedback-create,
-    .cw-structural-element-comment-create,
-    .cw-block-feedback-create,
-    .cw-block-comment-create {
-        border-top: none;
-    }
-}
-
-/* * * * * * * * * * * * * * * * * * * * * *
-c o m m e n t s  &  f e e d b a c k  e n d
-* * * * * * * * * * * * * * * * * * * * * * */
-
-/* * * * * * *
-w y s i w y g
-* * * * * * */
-textarea.studip-wysiwyg {
-    width: 100%
-}
-
-/* * * * * * * * * * *
-w y s i w y g  e n d
-* * * * * * * * * * */
-
-/* * * * * * * * *
-d a s h b o a r d
-* * * * * * * * */
-
-.cw-dashboard {
-    display: flex;
-    max-width: 1112px;
-    flex-wrap: wrap;
-
-    .cw-dashboard-box {
-        margin-bottom: 1em;
-        margin-right: 1em;
-
-        &.cw-dashboard-box-full {
-            max-width: $max-content-width;
-            width: calc(100% - 16px);
-        }
-        &.cw-dashboard-box-half {
-            width: calc(50% - 16px);
-        }
-
-        &.cw-collapsible .cw-collapsible-content.cw-collapsible-content-open {
-            padding: 0;
-        }
-    }
-    .cw-dashboard-overview {
-        display: flex;
-        padding: 10px;
-        flex-wrap: wrap;
-        justify-content: center;
-
-    }
-
-    &.cw-dashboard-task-view {
-        display: unset;
-        max-width: unset;
-        flex-wrap: unset;
-
-        .cw-dashboard-tasks-wrapper,
-        .cw-dashboard-students-wrapper {
-            max-height: unset;
-        }
-    }
-}
-
-#course-courseware-dashboard {
-    .action-menu-item a {
-        cursor: pointer;
-    }
-}
-
-.responsive-display {
-    .cw-dashboard {
-        .cw-dashboard-box {
-
-            &.cw-dashboard-box-full {
-                width: 100%
-            }
-            &.cw-dashboard-box-half {
-                width: 100%
-            }
-        }
-    }
-}
-
-.cw-activities-wrapper {
-    max-width: $max-content-width;
-
-    .cw-companion-box {
-        margin: 10px;
-    }
-
-    .cw-activities {
-        list-style: none;
-        padding: 0;
-
-        .cw-activity-item {
-            border-bottom: solid thin var(--content-color-40);
-            padding: 0.5em;
-
-            &:last-child {
-                border: none;
-            }
-
-            p {
-                margin: 0 0 4px 0;
-                img {
-                    padding-right: 0.5em;
-                    vertical-align: text-bottom;
-                }
-            }
-        }
-    }
-}
-
-.cw-dashboard-box {
-    .cw-dashboard-tasks-wrapper,
-    .cw-dashboard-students-wrapper {
-        padding: 10px;
-    }
-}
-
-.cw-dashboard-tasks-wrapper,
-.cw-dashboard-students-wrapper {
-
-    table.default {
-        margin: 0;
-        thead {
-            tr {
-                th {
-                    &.feedback {
-                        min-width: 11em;
-                    }
-                    &.renewal {
-                        min-width: 14em;
-                    }
-                }
-            }
-        }
-        tbody {
-            tr {
-                td {
-                    img {
-                        vertical-align: text-bottom;
-                        &.display-feedback,
-                        &.edit {
-                            cursor: pointer;
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-
-/* * * * * * * * * * * *
-d a s h b o a r d  e n d
-* * * * * * * * * * * */
-
-/* * * * * * * * * * * *
-    p r o g r e s s
-* * * * * * * * * * * */
-.cw-unit-progress {
-    .cw-unit-progress-breadcrumb {
-        padding: 10px 0;
-        a img {
-            vertical-align: top;
-        }
-    }
-
-    .cw-unit-progress-chapter {
-        text-align: center;
-        margin-bottom: -3.5em;
-
-        h1 {
-            border: none;
-            margin: 0;
-            padding: 0;
-        }
-
-        .cw-progress-circle {
-            font-size: 18px;
-            margin: 1em auto;
-
-            &.cw-unit-progress-current {
-                font-size: 12px;
-                top: -4.5em;
-                left: -2.5em;
-            }
-        }
-    }
-
-    .cw-unit-progress-subchapter-list {
-        border-top: solid thin var(--content-color-40);
-        padding: 0 1em 0 1em;
-
-        .cw-unit-empty-info {
-            margin-top: 10px;
-        }
-    }
-}
-
-.cw-unit-progress-item {
-    display: block;
-    border-bottom: solid thin var(--content-color-40);
-    padding: 10px 0;
-
-    &:hover{
-        background-color: hsla(217,6%,45%,.2);
-    }
-
-    &:last-child {
-        border: none;
-    }
-
-    .cw-unit-progress-item-value,
-    .cw-unit-progress-item-description {
-        display: inline-block;
-        vertical-align: top;
-    }
-
-    .cw-unit-progress-item-value {
-        width: 70px;
-        color: var(--base-color);
-        font-size: xx-large;
-
-        .cw-progress-circle {
-            font-size: 12px;
-            margin: 4px;
-        }
-    }
-    .cw-unit-progress-item-description {
-        color: var(--base-color);
-        padding: 0.5em 0 0 1em;
-        text-overflow: ellipsis;
-        overflow: hidden;
-        white-space: nowrap;
-    }
-}
-/* * * * * * * * * * * *
- p r o g r e s s  e n d
-* * * * * * * * * * * */
-
-/* * * * * * * * * * * * * * *
-p r o g r e s s  c i r c l e
-* * * * * * * * * * * * * * */
-
-.cw-progress-circle {
-    font-size: 14px;
-    margin: 10px;
-    position: relative;
-    padding: 0;
-    width: 5em;
-    height: 5em;
-    background-color: var(--dark-gray-color-10);
-    border-radius: 50%;
-    line-height: 5em;
-
-    &:after{
-        border: none;
-        position: absolute;
-        top: 0.35em;
-        left: 0.35em;
-        text-align: center;
-        display: block;
-        border-radius: 50%;
-        width: 4.3em;
-        height: 4.3em;
-        background-color: white;
-        content: " ";
-    }
-
-    span {
-        position: absolute;
-        line-height: 5em;
-        width: 5em;
-        text-align: center;
-        display: block;
-        color: var(--base-color);
-        z-index: 2;
-    }
-
-    .left-half-clipper {
-        border-radius: 50%;
-        width: 5em;
-        height: 5em;
-        position: absolute;
-        clip: rect(0, 5em, 5em, 2.5em);
-     }
-
-     &.over50 .left-half-clipper {
-        clip: rect(auto,auto,auto,auto);
-     }
-
-    .value-bar {
-        position: absolute;
-        clip: rect(0, 2.5em, 5em, 0);
-        width: 5em;
-        height: 5em;
-        border-radius: 50%;
-        border: 0.45em solid var(--base-color);
-        box-sizing: border-box;
-    }
-
-    &.over50 .first50-bar {
-        position: absolute;
-        clip: rect(0, 5em, 5em, 2.5em);
-        background-color: var(--base-color);
-        border-radius: 50%;
-        width: 5em;
-        height: 5em;
-    }
-
-    &:not(.over50) .first50-bar {
-        display: none;
-    }
-
-    &.p0 .value-bar { display: none; }
-
-    @for $i from 1 through 100 {
-        &.p#{$i} .value-bar {
-            transform: rotate(calc(360deg * $i / 100));
-        }
-    }
-}
-
-/* * * * * * * * * * * * * * * * * *
-p r o g r e s s  c i r c l e  e n d
-* * * * * * * * * * * * * * * * * */
-
-
-
-.cw-import-zip {
-    margin-bottom: 1em;
-
-    header {
-        font-size: 1.15;
-        font-weight: 700;
-    }
-    .progress-bar-wrapper {
-        width: 100%;
-        border: solid thin var(--content-color-40);
-
-        .progress-bar {
-            display: flex;
-            flex-direction: column;
-            justify-content: center;
-            color: white;
-            text-align: center;
-            white-space: nowrap;
-            background-color: var(--base-color);
-        }
-    }
-}
-
-/* * * * * *
-b l o c k s
-* * * * * */
-
-.cw-block-title {
-    padding: 4px;
-    background-color: var(--content-color-20);
-    color: var(--base-color);
-    font-weight: 700;
-    text-align: center;
-    border: solid thin var(--content-color-40);
-    border-bottom: none;
-}
-/* * * * * * * * *
-b l o c k s  e n d
-* * * * * * * * */
-
-/* * * * * * * * * *
-a u d i o  b l o c k
-* * * * * * * * * * */
-.cw-block-audio {
-    .cw-audio-container {
-        border: solid thin var(--content-color-40);
-        padding-top: 1em;
-    }
-    .cw-audio-controls {
-        text-align: right;
-        padding: 0 0.5em;
-    }
-    .cw-audio-range {
-        margin: 0 5px 10px 0;
-        &::-moz-focus-outer {
-            border: 0;
-        }
-        &.ui-widget-content {
-            background-color: var(--base-color);
-        }
-        .ui-widget-header {
-            background-color: var(--dark-gray-color-5);
-        }
-        .ui-slider-handle {
-            border-radius: 20px;
-            width: 1em;
-            height: 1.7em;
-            top: -0.5em;
-            background-color: var(--dark-gray-color-20);
-            border-color: var(--content-color-40);
-            cursor: pointer;
-            margin-left: -2px;
-        }
-    }
-    .cw-audio-button {
-        border: solid thin var(--content-color-40);
-        background-color: var(--white);
-        background-repeat: no-repeat;
-        background-position: center center;
-        background-size: 24px;
-        min-height: 27px;
-        line-height: 130%;
-        padding: 5px 15px 5px 30px;
-        cursor: pointer;
-        font-size: 14px;
-        box-sizing: border-box;
-        text-align: center;
-        text-decoration: none;
-        vertical-align: bottom;
-        white-space: nowrap;
-        min-width: unset;
-        margin: 5px;
-        height: 46px;
-        width: 46px;
-        display: inline-block;
-
-        &:hover {
-            background-color: var(--base-color);
-        }
-
-        @each $button, $icon in $media-buttons {
-            &.cw-audio-#{$button}button {
-                @include background-icon($icon, clickable, 24);
-                &:hover {
-                    @include background-icon($icon, info-alt, 24);
-                }
-            }
-        };
-    }
-
-
-    .cw-audio-time {
-        position: relative;
-        top: -1em;
-        color: var(--base-gray);
-    }
-
-    .cw-audio-range {
-        display: block;
-        margin: 0 auto 1.5em;
-        -webkit-appearance: none;
-        position: relative;
-        overflow: hidden;
-        height: 18px;
-        width: 100%;
-        cursor: pointer;
-        border-radius: 0;
-    }
-
-    .cw-audio-range::-webkit-slider-runnable-track {
-        background: var(--dark-gray-color-20);
-    }
-
-    .cw-audio-range::-webkit-slider-thumb {
-        -webkit-appearance: none;
-        width: 9px; /* 1 */
-        height: 18px;
-        background: var(--white);
-        box-shadow: -100vw 0 0 100vw var(--base-color);
-        border: solid thin var(--content-color-40);
-    }
-
-    .cw-audio-range::-moz-range-track {
-        height: 18px;
-        background: var(--dark-gray-color-10);
-    }
-
-    .cw-audio-range::-moz-range-thumb {
-        background: var(--white);
-        height: 18px;
-        width: 9px;
-        border: solid thin var(--content-color-40);
-        border-radius: 0 !important;
-        box-shadow: -100vw 0 0 100vw var(--base-color);
-        box-sizing: border-box;
-    }
-
-    .cw-audio-range::-ms-fill-lower {
-        background: var(--base-color);
-    }
-
-    .cw-audio-range::-ms-thumb {
-        background: var(--white);
-        border: solid thin var(--content-color-40);
-        height: 18px;
-        width: 9px;
-        box-sizing: border-box;
-    }
-
-    .cw-audio-range::-ms-ticks-after {
-        display: none;
-    }
-
-    .cw-audio-range::-ms-ticks-before {
-        display: none;
-    }
-
-    .cw-audio-range::-ms-track {
-        background: var(--dark-gray-color-20);
-        color: transparent;
-        height: 18px;
-        border: none;
-    }
-
-    .cw-audio-range::-ms-tooltip {
-        display: none;
-    }
-    .cw-audio-playlist-wrapper {
-        margin-top: -1em;
-        padding-top: 1em;
-        border: solid thin var(--content-color-40);
-        border-top: none;
-
-        &.empty {
-            border: none;
-        }
-
-        .cw-audio-playlist {
-            padding-left: 0;
-            list-style: none;
-            cursor: pointer;
-
-            &.with-recorder {
-                border-bottom: solid thin var(--content-color-40);
-            }
-
-            li {
-                margin: 0 1em;
-                &:not(:last-child) {
-                    border-bottom: solid thin var(--dark-gray-color-30);
-                }
-
-                .cw-playlist-item {
-                    display: block;
-                    @include background-icon(file-audio2, clickable, 24);
-                    background-repeat: no-repeat;
-                    background-position: 1em center;
-
-                    margin: 1em 0;
-                    padding: 1em;
-                    padding-left: 4em;
-                    color: var(--base-color);
-                    &:hover {
-                        color: var(--active-color);
-                    }
-                    &.current-item {
-                        @include background-icon(play, clickable, 24);
-                        font-weight: 700;
-                        &.is-playing {
-                            @include background-icon(pause, clickable, 24);
-                        }
-                    }
-                }
-            }
-        }
-        .cw-audio-playlist-recorder {
-            padding: 1em;
-        }
-    }
-
-
-    .cw-audio-current-track {
-        @include background-icon(file-audio2, info, 96);
-        background-position: top center;
-        background-repeat: no-repeat;
-        width: 100%;
-        min-height: 140px;
-        margin: 1em 0 2em 0;
-        p {
-            text-align: center;
-            padding-top: 106px;
-        }
-    }
-    .cw-audio-empty {
-        @include background-icon(file, info, 96);
-        border: solid thin var(--content-color-40);
-        background-position: center 1em;
-        background-repeat: no-repeat;
-        min-height: 140px;
-        padding: 1em;
-        p {
-            text-align: center;
-            padding-top: 106px;
-        }
-    }
-}
-/* * * * * * * * * * * * * *
-a u d i o  b l o c k  e n d
-* * * * * * * * * * * * * */
-
-/* * * * * * * * * * * * * * * * * * * *
-f o r  m u l t i m e d i a  b l o c k s
-* * * * * * * * * * * * * * * * * * * */
-.cw-file-empty {
-    @include background-icon(file, info, 96);
-    border: solid thin var(--content-color-40);
-    background-position: center 1em;
-    background-repeat: no-repeat;
-    min-height: 140px;
-    padding: 1em;
-    p {
-        text-align: center;
-        padding-top: 106px;
-    }
-}
-
-/* * * * * * * * * * * * * * * * * * * * * * * *
-f o r  m u l t i m e d i a  b l o c k s  e n d
-* * * * * * * * * * * * * * * * * * * * * * * */
-
-/* * * * * * * * * *
-v i d e o  b l o c k
-* * * * * * * * * * */
-.cw-block-video {
-    video {
-        width: 100%;
-    }
-}
-/* * * * * * * * * * * * * *
-v i d e o  b l o c k  e n d
-* * * * * * * * * * * * * */
-
-/* * * * * * * * * * * * * * * * *
-b e f o r e  a f t e r  b l o c k
-* * * * * * * * * * * * * * * * */
-.cw-block-before-after {
-    .twentytwenty-container {
-        width: 100% !important;
-        z-index: 19;
-        .twentytwenty-handle {
-            z-index: 18;
-        }
-        .twentytwenty-overlay {
-            z-index: 17;
-        }
-        img {
-            width: 100%;
-            z-index: 16;
-        }
-    }
-}
-/* * * * * * * * * * * * * * * * * * * *
-b e f o r e  a f t e r  b l o c k  e n d
-* * * * * * * * * * * * * * * * * * * */
-
-/* * * * * * * * *
-c o d e  b l o c k
-* * * * * * * * */
-.cw-block-code {
-    pre {
-        margin-bottom: 0;
-    }
-
-    .hljs {
-      display: block;
-      overflow-x: auto;
-      padding: 0.5em;
-      background: var(--dark-gray-color-5);
-      color: black;
-      border: solid thin var(--content-color-40);
-    }
-
-    .hljs-comment,
-    .hljs-quote,
-    .hljs-variable {
-      color: var(--dark-green);
-    }
-
-    .hljs-keyword,
-    .hljs-selector-tag,
-    .hljs-selector-class,
-    .hljs-built_in,
-    .hljs-name,
-    .hljs-tag {
-      color: var(--base-color);
-      font-weight: 600;
-    }
-
-    .hljs-string,
-    .hljs-title,
-    .hljs-section,
-    .hljs-attribute,
-    .hljs-literal,
-    .hljs-template-tag,
-    .hljs-template-variable,
-    .hljs-type,
-    .hljs-addition {
-      color: var(--orange-80);
-      font-weight: 400;
-    }
-
-    .hljs-deletion,
-    .hljs-selector-attr,
-    .hljs-selector-pseudo,
-    .hljs-meta {
-      color: var(--petrol);
-      font-weight: 400;
-    }
-
-    .hljs-doctag {
-      color: var(--dark-gray-color-75);
-      font-weight: 400;
-    }
-
-    .hljs-attr {
-      color: var(--active-color);
-      font-weight: 400;
-    }
-
-    .hljs-symbol,
-    .hljs-bullet,
-    .hljs-link {
-      color: var(--petrol);
-      font-weight: 400;
-    }
-
-    .hljs-emphasis {
-      font-style: italic;
-      font-weight: 400;
-    }
-
-    .hljs-strong {
-      font-weight: 600;
-    }
-
-    .code-lang {
-        background: var(--dark-gray-color-5);
-        border: solid thin var(--content-color-40);
-        border-top: none;
-        padding: 5px 10px;
-        text-align: right;
-        color: var(--dark-gray-color-45);
-        font-family: monospace;
-        text-transform: full-width;
-    }
-
-}
-/* * * * * * * * * * * * *
-c o d e  b l o c k  e n d
-* * * * * * * * * * * * */
-
-/* * * * * * * * * * * * *
-c o n f i r m  b l o c k
-* * * * * * * * * * * * */
-.cw-block-confirm {
-    .cw-block-confirm-content{
-        border: solid thin var(--content-color-40);
-        padding: 1em;
-        margin: 0;
-
-        input[type=checkbox] {
-            margin-right: 2em;
-            vertical-align: bottom;
-        }
-    }
-}
-/* * * * * * * * * * * * * * * *
-c o n f i r m  b l o c k  e n d
-* * * * * * * * * * * * * * * */
-
-/* * * * * * * * * *
-d a t e  b l o c k
-* * * * * * * * * */
-
-.cw-container-colspan-half,
-.cw-container-colspan-half-center {
-    .cw-block-date {
-        .cw-block-content {
-            font-size: 9px;
-        }
-    }
-}
-
-.cw-block-date {
-
-    .cw-date-countdown,
-    .cw-date-date {
-        margin: 0 auto;
-        width: max-content;
-    }
-
-    .cw-date-countdown {
-        .cw-date-countdown-digit {
-            display: inline-block;
-            margin-right: 4px;
-
-            .cw-date-countdown-number {
-                font-size: 6em;
-                line-height: 1.25em;
-                height: 1.25em;
-                padding: 3px 0.5em;
-                background: rgba(245,245,245,1);
-                font-weight: 300;
-            }
-
-            .cw-date-countdown-label-sg,
-            .cw-date-countdown-label-pl {
-                padding: 5px;
-                font-size: 1.25em;
-                text-align: left;
-                text-transform: uppercase;
-            }
-        }
-    }
-
-    .cw-date-date {
-        .cw-date-date-space {
-            display: inline-block;
-            width: 2em;
-
-        }
-
-        .cw-date-date-digits {
-            display: inline-block;
-
-            .cw-date-date-number {
-                font-size: 5em;
-                line-height: 1.25em;
-                height: 1.25em;
-                padding: 0.25em;
-                background: rgba(245,245,245,1);
-                font-weight: 300;
-            }
-        }
-    }
-}
-
-.responsive-display {
-    .cw-block-date {
-        .cw-block-content {
-            font-size: 9px;
-
-            .cw-date-countdown-number {
-                font-size: 3.5em;
-            }
-            .cw-date-countdown-label-sg,
-            .cw-date-countdown-label-pl {
-                font-size: 1em;
-            }
-            .cw-date-date-number {
-                font-size: 3em;
-            }
-        }
-    }
-}
-
-/* * * * * * * * * * * * *
-d a t e  b l o c k  e n d
-* * * * * * * * * * * * */
-
-/* * * * * * * * * * * *
-c a n v a s  b l o c k
-* * * * * * * * * * * */
-.cw-block-canvas {
-    .cw-canvasblock-canvas {
-        max-width: 100%;
-        border: solid thin var(--content-color-40);
-    }
-
-    .cw-canvasblock-upload-message{
-        display: none;
-    }
-
-    .cw-canvasblock-original-img {
-        display: none;
-    }
-
-    .cw-canvasblock-tool-selected-text {
-        cursor: text;
-    }
-
-    h1.cw-canvasblock-description {
-        border-bottom: none;
-    }
-
-    .cw-canvasblock-toolbar {
-        border: solid thin var(--content-color-40);
-        border-bottom: none;
-
-        .cw-canvasblock-buttonset {
-            display: inline-block;
-            padding: 5px;
-            margin-right: 0.5em;
-    
-            button {
-                cursor: pointer;
-                user-select: none;
-                border: solid thin var(--content-color-40);
-                height: 32px;
-                width: 32px;
-                background-color: var(--white);
-                background-position: center;
-                background-repeat: no-repeat;
-                background-size: 24px 24px;
-        
-                &.cw-canvasblock-color {
-                    $colors: (
-                        white: #ffffff,
-                        blue: #3498db,
-                        green: #2ecc71,
-                        purple: #9b59b6,
-                        red: #e74c3c,
-                        yellow: #fed330,
-                        orange: #f39c12,
-                        grey: #95a5a6,
-                        darkgrey: #34495e,
-                        black: #000000,
-                    );
-        
-                    @each $name, $color in $colors {
-                        &.#{"" + $name} {
-                            background-color: $color;
-                        }
-                    }
-        
-                    &.selected-color {
-                        border: solid 2px var(--black);
-                    }
-                }
-        
-                &.cw-canvasblock-reset {
-                    @include background-icon(refresh, clickable, 24);
-                }
-        
-                &.cw-canvasblock-size {
-                    @include background-icon(stop, clickable);
-        
-                    &.cw-canvasblock-size-small {
-                        background-size: 8px 7px;
-                    }
-                    &.cw-canvasblock-size-normal {
-                        background-size: 16px 14px;
-                    }
-                    &.cw-canvasblock-size-large {
-                        background-size: 22px 20px;
-                    }
-                    &.cw-canvasblock-size-huge {
-                        background-size: 26px 24px;
-                    }
-                    &.selected-size {
-                        border: solid 2px var(--black);
-                    }
-                }
-        
-                &.cw-canvasblock-tool {
-                    &.cw-canvasblock-tool-pen {
-                        @include background-icon(comment, clickable);
-                    }
-        
-                    &.cw-canvasblock-tool-text {
-                        vertical-align: top;
-                        font-size: 22px;
-                        color: var(--base-color);
-                        font-weight: 600;
-                    }
-        
-                    &.selected-tool {
-                        border: solid 2px var(--black);
-                    }
-                }
-        
-                &.cw-canvasblock-undo {
-                    @include background-icon(arr_2left, clickable, 24);
-                }
-        
-                &.cw-canvasblock-download {
-                    @include background-icon(download, clickable, 24);
-                }
-                &.cw-canvasblock-store {
-                    @include background-icon(upload, clickable, 24);
-                }
-                &.cw-canvasblock-show-all {
-                    @include background-icon(group2, clickable, 24);
-                    &.selected-view {
-                        border: solid 2px var(--black);
-                    }
-                }
-                &.cw-canvasblock-show-own {
-                    @include background-icon(person, clickable, 24);
-                    &.selected-view {
-                        border: solid 2px var(--black);
-                    }
-                }
-            }
-        }
-    }
-
-    .cw-canvasblock-tool-selected-text {
-        cursor: text;
-    }
-}
-/* * * * * * * * * * * * * * *
-c a n v a s  b l o c k  e n d
-* * * * * * * * * * * * * * */
-
-/* * * * * * * * * * * * *
-d o c u m e n t  b l o c k
-* * * * * * * * * * * * */
-.cw-block-document {
-    .cw-pdf-main-container {
-        width: calc(100% - 2px);
-        border: solid thin var(--content-color-40);
-        .cw-block-title {
-            border: none;
-            border-bottom: solid thin var(--content-color-40);
-        }
-    }
-    .cw-pdf-toolbar {
-        position: relative;
-        display: flex;
-        flex-direction: row;
-        justify-content: flex-start;
-        align-items: baseline;
-        align-content: space-around;
-        background-color: var(--content-color-20);
-        padding: 4px 8px;
-
-        button {
-            height: 100%;
-            margin: 0 2px 0 0;
-            padding: 4px;
-            
-            &.active {
-                background-color: var(--base-color);
-            }
-        }
-
-        .cw-pdf-toolbar-left {
-            position: relative;
-            display: flex;
-            flex-direction: row;
-            justify-content: flex-start;
-            align-items: baseline;
-            align-content: space-between;
-            width: 33%;
-        }
-        .cw-pdf-toolbar-middle {
-            position: relative;
-            display: flex;
-            justify-content: center;
-            width: 34%;
-
-            .cw-pdf-zoom-buttons {
-                margin-right: 8px;
-
-                button {
-                    margin: 0;
-                    padding: 4px 0;
-                }
-            }
-        }
-        .cw-pdf-toolbar-right {
-            display: flex;
-            flex-direction: row;
-            justify-content: flex-end;
-            align-items: baseline;
-            align-content: space-between;
-            position: relative;
-            width: 33%;
-            margin-right: 4px;
-
-        }
-        .cw-pdf-page-nav {
-            margin: 0 4px;
-
-            button {
-                margin: 0;
-                padding: 4px 0;
-            }
-            .cw-pdf-page-num {
-                text-align: right;
-                width: 2em;
-            }
-        }
-
-        .cw-pdf-search-box {
-            position: absolute;
-            top: 33px;
-            left: 22px;
-            width: auto;
-            background-color: var(--content-color-20);
-            border-top: none;
-            padding: 6px;
-            z-index: 2;
-            line-height: normal;
-
-            .cw-pdf-search-num {
-                margin: 4px 0 0 0;
-                display: block;
-            }
-            .cw-pdf-search-navs {
-                display: inline-block;
-                button {
-                    margin: 0;
-                    padding: 0;
-                }
-            }
-        }
-    }
-    .cw-pdf-outer-container {
-        position: relative;
-        width: 100%;
-
-        .cw-pdf-content {
-            display: flex;
-            flex-direction: row;
-
-            .cw-pdf-sidebar {
-                width: 25%;
-                min-width: 270px;;
-                align-self: stretch;
-                background-color: var(--white);
-                border-right: solid 1px var(--content-color-40);
-    
-                ul.cw-pdf-toc-list, ul.cw-pdf-toc-sub-list {
-                    padding: 0;
-                    list-style: none;
-    
-                    li {
-                        padding: 0.5em 1em;
-                    }
-                }
-                ul.cw-pdf-toc-list {
-                    margin-top: 1em;
-                }
-            }
-
-            .cw-pdf-viewer-container {
-                width: 100%;
-                height: 100%;
-                overflow: hidden;
-                cursor: text;
-    
-                &.hand-cursor-grab {
-                    cursor: grab;
-                    &.grabbing {
-                        cursor: grabbing;
-                    }
-                }
-                &.has-error {
-                    display: none;
-                }
-                .page {
-                    position: relative;
-                    margin: 0 auto;
-                }
-            }
-
-        }
-
-
-        .cw-pdf-viewer-fake-container {
-            position: absolute;
-        }
-
-
-        .cw-pdf-error-page {
-            overflow: hidden;
-            width: calc(100% - 16px);
-            height: 100%;
-            padding: 8px;
-            display:table;
-        }
-    }
-}
-/* * * * * * * * * * * * * * * * * *
-d o c u m e n t  b l o c k  e n d
-* * * * * * * * * * * * * * * * * */
-
-/* * * * * * * * * * *
-e m b e d  b l o c k
-* * * * * * * * * * */
-
-.cw-block-embed {
-    .cw-block-content {
-        .cw-block-embed-iframe-wrapper {
-            overflow-y: hidden;
-            iframe{
-                height: 100% !important;
-                width: 100% !important;
-            }
-        }
-        .cw-block-embed-info {
-            margin-top: 0.5em;
-        }
-    }
-}
-
-/* * * * * * * * * * * * * *
-e m b e d  b l o c k  e n d
-* * * * * * * * * * * * * */
-
-/* * * * * * * * * * *
-i f r a m e  b l o c k
-* * * * * * * * * * */
-
-.cw-block-iframe {
-    .cw-block-content {
-        iframe {
-            border: solid thin var(--content-color-40);
-            width: calc(100% - 2px);
-        }
-
-        .cw-block-iframe-cc-data {
-            border: solid thin var(--content-color-40);
-            border-top: none;
-            margin-top: -6px;
-            padding-top: 10px;
-            height: 75px;
-
-            .cw-block-iframe-cc {
-                width: 120px;
-                height: 50px;
-                margin-left: 4px;
-                display: inline-block;
-                background-repeat: no-repeat;
-                &.cw-block-iframe-cc-by {
-                    background-image: url("#{$image-path}/cc/by.svg");
-                }
-                &.cw-block-iframe-cc-by-nc {
-                    background-image: url("#{$image-path}/cc/by-nc.eu.svg");
-                }
-                &.cw-block-iframe-cc-by-nc-nd {
-                    background-image: url("#{$image-path}/cc/by-nc-nd.eu.svg");
-                }
-                &.cw-block-iframe-cc-by-nc-sa {
-                    background-image: url("#{$image-path}/cc/by-nc-sa.svg");
-                }
-                &.cw-block-iframe-cc-by-nd {
-                    background-image: url("#{$image-path}/cc/by-nd.svg");
-                }
-                &.cw-block-iframe-cc-by-sa {
-                    background-image: url("#{$image-path}/cc/by-sa.svg");
-                }
-            }
-            .cw-block-iframe-cc-infos{
-                display: inline-block;
-                vertical-align: top;
-                padding-left: 1em;
-                p {
-                    margin: 0;
-                }
-            }
-        }
-
-
-
-    }
-}
-
-/* * * * * * * * * * * * * *
-i f r a m e  b l o c k  e n d
-* * * * * * * * * * * * * */
-
-/* * * * * * * * +
-l t i  b l o c k
-* * * * * * * * */
-.cw-block-lti {
-    .cw-block-content {
-        .cw-block-lti-content {
-            border: solid thin var(--content-color-40);
-            box-sizing: border-box;
-        }
-        .cw-block-lti-icon-tool {
-            @include background-icon(plugin, info, 24);
-            background-repeat: no-repeat;
-
-            display: block;
-            padding: 16px 16px 16px 40px;
-            background-position: 10px center;
-            overflow: hidden;
-            text-overflow: ellipsis;
-        }
-    }
-}
-/* * * * * * * * * * * *
-l t i  b l o c k  e n d
-* * * * * * * * * * * */
-
-/* * * * * * * * * * * *
-f o l d e r  b l o c k
-* * * * * * * * * * * */
-.cw-block-folder-info {
-    border: solid thin var(--content-color-40);
-    padding: 10px 10px 0 10px;
-    overflow: hidden;
-    border-bottom: none;
-}
-.cw-block-folder-list {
-    border: solid thin var(--content-color-40);
-    padding: 0;
-    list-style: none;
-
-    .cw-block-folder-file-item {
-        list-style: none;
-
-        &:not(:last-child) {
-            border-bottom: solid thin var(--content-color-40);
-        }
-        a {
-            display: block;
-        }
-        &:hover {
-            background-color: hsla(217,6%,45%,.2);
-        }
-    }
-    .cw-block-folder-download-icon {
-        @include background-icon(download, clickable, 24);
-        background-repeat: no-repeat;
-
-        float: right;
-        height: 24px;
-        width: 24px;
-        margin: 1em;
-    }
-}
-.cw-block-folder-upload {
-    border: solid thin var(--content-color-40);
-    padding: 1em 10px;
-    border-top: none;
-
-    .cw-file-input {
-        width: calc(100% - 148px);
-        vertical-align: middle;
-    }
-}
-// for folder and download block
-.cw-block-file-info {
-    @include background-icon(file, clickable, 24);
-    background-repeat: no-repeat;
-
-    display: block;
-    padding: 16px 16px 16px 40px;
-    background-position: 10px 16px;
-    width: calc(100% - 56px);
-    overflow: hidden;
-    text-overflow: ellipsis;
-
-    &.cw-block-file-icon-empty {
-        color: var(--black);
-        @include background-icon(folder-empty, info, 24);
-    }
-    &.cw-block-file-icon-none {
-        color: var(--black);
-        @include background-icon(file, info, 24);
-    }
-    &.cw-block-file-icon-audio {
-        @include background-icon(file-audio, clickable, 24);
-        &.download-disabled {
-            @include background-icon(file-audio, info, 24);
-        }
-    }
-    &.cw-block-file-icon-pic {
-        @include background-icon(file-pic, clickable, 24);
-        &.download-disabled {
-            @include background-icon(file-pic, info, 24);
-        }
-    }
-    &.cw-block-file-icon-video {
-        @include background-icon(file-video, clickable, 24);
-        &.download-disabled {
-            @include background-icon(file-video, info, 24);
-        }
-    }
-    &.cw-block-file-icon-pdf {
-        @include background-icon(file-pdf, clickable, 24);
-        &.download-disabled {
-            @include background-icon(file-pdf, info, 24);
-        }
-    }
-    &.cw-block-file-icon-word {
-        @include background-icon(file-word, clickable, 24);
-        &.download-disabled {
-            @include background-icon(file-word, info, 24);
-        }
-    }
-    &.cw-block-file-icon-spreadsheet {
-        @include background-icon(file-excel, clickable, 24);
-        &.download-disabled {
-            @include background-icon(file-excel, info, 24);
-        }
-    }
-    &.cw-block-file-icon-text {
-        @include background-icon(file-text, clickable, 24);
-        &.download-disabled {
-            @include background-icon(file-text, info, 24);
-        }
-    }
-    &.cw-block-file-icon-ppt {
-        @include background-icon(file-ppt, clickable, 24);
-        &.download-disabled {
-            @include background-icon(file-ppt, info, 24);
-        }
-    }
-    &.cw-block-file-icon-archive {
-        @include background-icon(file-archive, clickable, 24);
-        &.download-disabled {
-            @include background-icon(file-archive, info, 24);
-        }
-    }
-    &.cw-block-file-icon-file {
-        @include background-icon(file, clickable, 24);
-        &.download-disabled {
-            @include background-icon(file, info, 24);
-        }
-    }
-}
-.cw-block-file-details {
-    margin-top: -16px;
-    padding-left: 40px;
-    padding-bottom: 16px;
-    color: var(--dark-gray-color);
-}
-.cw-block-file-owner,
-.cw-block-file-mkdate {
-    display: block;
-    width: calc(100% - 56px);
-    overflow: hidden;
-    text-overflow: ellipsis;
-}
-/* * * * * * * * * * * * * * *
-f o l d e r  b l o c k  e n d
-* * * * * * * * * * * * * * */
-
-
-/* * * * * * * * * * * * * *
-d o w n l o a d  b l o c k
-* * * * * * * * * * * * * */
-.cw-block-download {
-    .cw-block-download-content {
-        border: solid thin var(--content-color-40);
-        .cw-block-download-file-item {
-            a {
-                display: block;
-            }
-            &:hover {
-                background-color: fade-out($dark-gray-color-75, 0.8);
-            }
-            .cw-block-download-download-icon {
-                @include background-icon(download, clickable, 24);
-                background-repeat: no-repeat;
-
-                float: right;
-                height: 24px;
-                width: 24px;
-                margin: 1em;
-            }
-        }
-    }
-
-}
-
-/* * * * * * * * * * * * * * * * *
-d o w n l o a d  b l o c k  e n d
-* * * * * * * * * * * * * * * * */
-
-/* * * * * * * * * * * * *
-g a l l e r y  b l o c k
-* * * * * *  * * * * * * */
-
-.cw-block-gallery {
-    .cw-block-content {
-        overflow: hidden;
-    }
-}
-
-.cw-block-gallery-content {
-    position: relative;
-    margin: auto;
-}
-
-.cw-block-gallery-slides {
-    display: none;
-    img {
-        display: block;
-        max-width: 100%;
-        margin-left: auto;
-        margin-right: auto;
-    }
-}
-
-.cw-block-gallery-prev,
-.cw-block-gallery-next {
-    cursor: pointer;
-    position: absolute;
-    background-color: fade-out($white, 0.6);
-    top: 50%;
-    height: 36px;
-    width: 36px;
-    background-repeat: no-repeat;
-    background-position: center;
-    margin-top: -22px;
-    transition: 200ms ease;
-    user-select: none;
-    border: none;
-
-    &:hover {
-        background-color: var(--base-color);
-    }
-}
-
-.cw-block-gallery-prev {
-    @include background-icon(arr_1left, clickable, 24);
-    &:hover{
-        @include background-icon(arr_1left, info-alt, 24);
-    }
-}
-.cw-block-gallery-next {
-    right: 0;
-    @include background-icon(arr_1right, clickable, 24);
-    &:hover {
-        @include background-icon(arr_1right, info-alt, 24);
-    }
-}
-
-.cw-block-gallery-file-description {
-    width: -moz-available;
-    color: var(--white);
-    font-size: 15px;
-    padding: 8px 12px;
-    position: absolute;
-    bottom: 8px;
-    text-align: center;
-    p {
-        display: -webkit-inline-box;
-        background-color: fade-out($black, 0.6);
-        padding: 0 2em;
-        margin-bottom: 4px;
-        overflow: hidden;
-        -webkit-line-clamp: 3;
-        -webkit-box-orient: vertical;
-    }
-
-    &.show-on-hover {
-        display: none;
-    }
-}
-
-.cw-block-gallery-content:hover .show-on-hover {
-    display: block;
-}
-
-.cw-block-gallery-number-text {
-    color: var(--white);
-    font-size: 12px;
-    padding: 8px 12px;
-    position: absolute;
-    top: 0;
-    background-color: fade-out($black, 0.6);
-}
-
- .cw-block-gallery-fade {
-    -webkit-animation-name: fade;
-    -webkit-animation-duration: 1.5s;
-    animation-name: fade;
-    animation-duration: 1.5s;
-}
-
-@-webkit-keyframes fade {
-    from {opacity: .4}
-    to {opacity: 1}
-}
-
-@keyframes fade {
-    from {opacity: .4}
-    to {opacity: 1}
-}
-
-
-.cw-block-gallery-grid {
-    display: flex;
-    flex-direction: row;
-    flex-wrap: wrap;
-    justify-content: flex-start;
-    list-style: none;
-    padding: 0;
-    figure {
-        padding: 1px 4px;
-        margin: unset;
-        figcaption {
-            margin-bottom: 12px;
-
-            .cw-block-gallery-grid-file-name {
-                font-weight: 700;
-                margin-bottom: 4px;
-                overflow: hidden;
-                display: -webkit-box;
-                -webkit-line-clamp: 2;
-                -webkit-box-orient: vertical;
-            }
-            .cw-block-gallery-grid-file-description {
-                overflow: hidden;
-                display: -webkit-box;
-                -webkit-line-clamp: 5;
-                -webkit-box-orient: vertical;
-            }
-        }
-    }
-}
-.cw-container-wrapper-edit .cw-block-gallery-grid {
-    margin: 0;
-}
-
-/* * * * * * * * * * * * * * * * *
-g a l l e r y  b l o c k  e n d
-* * * * * * * * * * * * * * * * */
-
-
-/* * * * * * * * * * * * * * * * *
-i m a g e  m a p  b l o c k
-* * * * * * * * * * * * * * * * */
-.cw-block-image-map {
-    .cw-image-map-canvas,
-    .cw-image-map-original-img {
-      display: none;
-    }
-
-    form.default {
-        label.cw-block-image-map-dimensions > input[type=number] {
-            display: inline-block;
-        }
-    }
-}
-
-/* * * * * * * * * * * * * * * * *
-i m a g e  m a p  b l o c k  e n d
-* * * * * * * * * * * * * * * * */
-
-/* * * * * * * * * * * * * *
-b i o g r a p h y  b l o c k
-* * * * * * * * * * * * * */
-
-.cw-block-biography {
-    .cw-block-biography-content {
-        display: flex;
-        min-height: 200px;
-        flex-direction: row;
-        padding: 2em 2em 2em 1em;
-        border: 2px solid var(--base-color);
-
-        .cw-block-biography-type {
-            margin: auto 1em auto 0;
-            padding-top: 96px;
-            min-width: 192px;
-            max-width: 192px;
-            text-align: center;
-            background-repeat: no-repeat;
-            background-position: center top;
-        }
-
-        .cw-block-biography-details {
-            h2, h3 {
-                margin-top: 0;
-            }
-        }
-
-    }
-}
-
-/* * * * * * * * * * * * * * * * * *
-b i o g r a p h y  b l o c k  e n d
-* * * * * * * * * * * * * * * * * */
-
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * *
-b i o g r a p h y  a c h i e v e m e n t s  b l o c k
-* * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-.cw-block-biography-achievements {
-    @each $type, $icon in $achievement-types {
-        .cw-block-biography-achievements-type-#{$type} {
-            @include background-icon($icon, clickable, 96);
-        }
-    }
-}
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
-b i o g r a p h y  a c h i e v e m e n t s  b l o c k  e n d
-* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-/* * * * * * * * * * * * ** * * * * * *
-b i o g r a p h y  g o a l s  b l o c k
-* * * * * * * * * * * * * * * * * * * */
-
-.cw-block-biography-goals {
-    @each $type, $icon in $goals-types {
-        .cw-block-biography-goals-type-#{$type} {
-            @include background-icon($icon, clickable, 96);
-        }
-    }
-}
-
-/* * * * * * * * * * * * * * * * * * * * * * *
-b i o g r a p h y  g o a l s  b l o c k  e n d
-* * * * * * * * * * * * * * * * * * * * * * */
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
-b i o g r a p h y  p e r s o n a l  i n f o r m a t i o n  b l o c k
-* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-.cw-block-biography-personal-information {
-    .cw-block-biography-content {
-        min-height: 140px;
-
-        .cw-block-biography-personal-information-type {
-            @include background-icon(person2, clickable, 96);
-        }
-
-        .cw-block-biography-personal-information-details {
-            display: grid;
-            max-height: 7em;
-            grid-template-columns: max-content 1fr;
-            grid-gap: 5px 10px;
-
-            .preface {
-                grid-column-start: 1;
-                grid-column-end: 3;
-            }
-        }
-    }
-}
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
-b i o g r a p h y  p e r s o n a l  i n f o r m a t i o n  b l o c k  e n d
-* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-/* * * * * * * * * *
-l i n k  b l o c k
-* * * * * * * * * */
-
-.cw-block-link {
-    a {
-        text-decoration: none;
-    }
-    .cw-link {
-        border: solid thin var(--content-color-40);
-        color: var(--base-color);
-        height: 20px;
-        padding: 1em;
-
-        .cw-link-title {
-            margin-left: 3em;
-        }
-
-        &:hover {
-            background-color: var(--base-color);
-            border: solid thin var(--base-color);
-            color: var(--white);
-        }
-
-        &.internal {
-            @include background-icon(link-intern, clickable, 28);
-            background-position: 1em 50%;
-            background-repeat: no-repeat;
-
-            &:hover {
-                @include background-icon(link-intern, info-alt, 28);
-            }
-        }
-
-        &.external {
-            @include background-icon(link-extern, clickable, 28);
-            background-position: 1em 50%;
-            background-repeat: no-repeat;
-            &:hover {
-                @include background-icon(link-extern, info-alt, 28);
-            }
-
-            .cw-link-og-image {
-                display: inline-block;
-                max-width: 40%;
-                vertical-align: top;
-                margin-right: 2em;
-            }
-            .cw-link-og-textwrapper {
-                display: inline-block;
-                max-width: 50%;
-                p {
-                    margin: 0;
-                }
-                .cw-link-og-site {
-                    font-size: 1.25em;
-                }
-                .cw-link-og-title {
-                    font-weight: 600;
-                }
-                .cw-link-og-description {
-                    color: var(--base-color-80);
-                    text-align: justify;
-                }
-            }
-        }
-    }
-}
-
-/* * * * * * * * * * * * *
-l i n k  b l o c k  e n d
-* * * * * * * * * * * * */
-
-/*
-dialog cards block
-*/
-.cw-block-dialog-cards-content {
-    display: flex;
-    .cw-dialogcards {
-        flex-grow: 2;
-        .scene {
-            margin: 0 auto;
-            width: 440px;
-            height: 600px;
-            perspective: 880px;
-            display: none;
-            &.active {
-                display: block;
-                animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
-                transform: translate3d(0, 0, 0);
-                backface-visibility: hidden;
-                perspective: 1000px;
-            }
-        }
-
-        .card {
-            width: 100%;
-            height: 78%;
-            transition: transform 1s;
-            transform-style: preserve-3d;
-            cursor: pointer;
-            position: relative;
-            top: 11%;
-        }
-
-        .card.is-flipped {
-            transform: rotateY(180deg);
-        }
-
-        .card__face {
-            position: absolute;
-            width: 100%;
-            height: 100%;
-            color: var(--black);
-            text-align: center;
-            font-weight: bold;
-            font-size: 1.2em;
-            backface-visibility: hidden;
-            box-shadow: 0 2px 15px fade-out($black, 0.7);
-
-            img {
-                max-width: 380px;
-                max-height: 220px;
-                margin-top: 1em;
-            }
-
-            .cw-dialogcards-front-no-image {
-                @include background-icon(question, navigation, 150);
-            }
-
-            .cw-dialogcards-back-no-image {
-                @include background-icon(exclaim, navigation, 150);
-            }
-
-            .cw-dialogcards-front-no-image,
-            .cw-dialogcards-back-no-image {
-                width: 100%;
-                height: 180px;
-                margin-top: 2em;
-                background-repeat: no-repeat;
-                background-position-x: center;
-            }
-
-            p {
-                margin: 1em 3em 1em 4em;
-                padding-right: 1em;
-                overflow-y: auto;
-                max-height: 12em;
-                text-align: justify;
-            }
-        }
-
-        .card__face--front {
-            @include background-icon(arr_1right, clickable);
-            background-color: var(--white);
-            background-repeat: no-repeat;
-            background-position: 95% 95%;
-        }
-
-        .card__face--back {
-            @include background-icon(arr_1left, clickable);
-            background-color: var(--white);
-            background-repeat: no-repeat;
-            background-position: 5% 95%;
-            transform: rotateY(180deg);
-        }
-    }
-
-    .cw-dialogcards-navbutton {
-        color: transparent;
-        width: 35px;
-        height: 35px;
-        border-radius: 2px;
-        background-position: 50%;
-        background-repeat: no-repeat;
-        background-color: var(--base-color);
-        border: none;
-        display: block;
-        z-index: 4;
-        margin: auto 2px;
-        padding: 0;
-        cursor: pointer;
-
-        &.cw-dialogcards-prev {
-            @include background-icon(arr_1left, info-alt, 24);
-
-        }
-
-        &.cw-dialogcards-next {
-            @include background-icon(arr_1right, info-alt, 24);
-            right: 0;
-        }
-
-        &.cw-dialogcards-prev-disabled,
-        &.cw-dialogcards-next-disabled {
-            background-color: var(--light-gray-color-40);
-        }
-    }
-
-    @keyframes shake {
-        10%, 90% {
-            transform: translate3d(-1px, 0, 0);
-        }
-
-        20%, 80% {
-            transform: translate3d(2px, 0, 0);
-        }
-
-        30%, 50%, 70% {
-            transform: translate3d(-4px, 0, 0);
-        }
-
-        40%, 60% {
-            transform: translate3d(4px, 0, 0);
-        }
-    }
-}
-
-
-/*
-dialog cards block end
-*/
-
-/*
-headline block
-*/
-$big-icon-size: 196;
-$big-icon-size-px: $big-icon-size + px;
-$large-icon-size: 128;
-$large-icon-size-px: $large-icon-size + px;
-
-.cw-block-headline {
-    .cw-block-headline-content {
-        min-height: 600px;
-        overflow: hidden;
-        background-position: center;
-        background-size: $max-content-width;
-        background-repeat: no-repeat;
-
-        &.half {
-            min-height: 300px;
-        }
-
-        &.quarter {
-            min-height: 150px;
-        }
-
-        &.heavy {
-
-            .cw-block-headline-iconbox {
-                display: none;
-            }
-            .cw-block-headline-textbox {
-                margin: 2em;
-                h1, h2 {
-                    display: inline;
-                    border: none;
-                    padding: 0.25em 0;
-                }
-                h1 {
-                    font-size: 10em;
-                    line-height: 1.2em;
-                }
-                h2 {
-                    font-size: 2em;
-                    line-height: 1em;
-                }
-
-                &.quarter {
-                    margin: 1.5em;
-                    h1 {
-                        font-size: 5em;
-                    }
-                    h2 {
-                        font-size: 1.5em;
-                    }
-                }
-            }
-
-        }
-        &.bigicon_top {
-            .icon-layer {
-                background-repeat: no-repeat;
-                background-position: center;
-                margin-top: 8em;
-                margin-bottom: 1em;
-                background-size: $big-icon-size-px;
-                height: $big-icon-size-px;
-
-                @each $icon in $icons {
-                    &.icon-black-#{$icon} {
-                        @include background-icon($icon, info, $big-icon-size);
-                    }
-                    &.icon-white-#{$icon} {
-                        @include background-icon($icon, info-alt, $big-icon-size);
-                    }
-                    &.icon-studip-blue-#{$icon} {
-                        @include background-icon($icon, clickable, $big-icon-size);
-                    }
-                    &.icon-studip-red-#{$icon} {
-                        @include background-icon($icon, status-red, $big-icon-size);
-                    }
-                    &.icon-studip-yellow-#{$icon} {
-                        @include background-icon($icon, status-yellow, $big-icon-size);
-                    }
-                    &.icon-studip-green-#{$icon} {
-                        @include background-icon($icon, status-green, $big-icon-size);
-                    }
-                    &.icon-studip-gray-#{$icon} {
-                        @include background-icon($icon, inactive, $big-icon-size);
-                    }
-                };
-
-               &.half {
-                    height: 144px;
-                    background-size: 144px;
-                    background-position: center;
-                    margin-top: 2em;
-                    margin-bottom: 0;
-                }
-                &.quarter {
-                    height: 72px;
-                    background-size: 72px;
-                    background-position: center;
-                    margin-top: 1em;
-                    margin-bottom: 0;
-                }
-            }
-
-            .cw-block-headline-textbox {
-                width: 80%;
-                padding-bottom: 4em;
-                margin: 0 auto;
-                .cw-block-headline-title {
-                    h1 {
-                        border: none;
-                        font-size: 5em;
-                        text-align: center;
-                    }
-                }
-
-                .cw-block-headline-subtitle {
-                    h2 {
-                        border: none;
-                        font-size: 18px;
-                        text-align: center;
-                        margin-top: 10px;
-                    }
-                }
-
-                &.quarter {
-                    padding-bottom: 1em;
-                    .cw-block-headline-title {
-                        h1 {
-                            font-size: 2.25em;
-                            margin: 0;
-                        }
-                    }
-                    .cw-block-headline-subtitle {
-                        h2 {
-                            font-size: 1em;
-                            margin: 0;
-                        }
-                    }
-                }
-            }
-        }
-
-        &.bigicon_before {
-            display: flex;
-            align-items: center;
-            .icon-layer {
-                min-height: $big-icon-size-px;
-                min-width: $big-icon-size-px;
-                margin:0 28px 0 28px;
-                background-repeat: no-repeat;
-                background-position: left center;
-
-                @each $icon in $icons {
-                    &.icon-black-#{$icon} {
-                        @include background-icon($icon, info, $big-icon-size);
-                    }
-                    &.icon-white-#{$icon} {
-                        @include background-icon($icon, info-alt, $big-icon-size);
-                    }
-                    &.icon-studip-blue-#{$icon} {
-                        @include background-icon($icon, clickable, $big-icon-size);
-                    }
-                    &.icon-studip-red-#{$icon} {
-                        @include background-icon($icon, status-red, $big-icon-size);
-                    }
-                    &.icon-studip-yellow-#{$icon} {
-                        @include background-icon($icon, status-yellow, $big-icon-size);
-                    }
-                    &.icon-studip-green-#{$icon} {
-                        @include background-icon($icon, status-green, $big-icon-size);
-                    }
-                    &.icon-studip-gray-#{$icon} {
-                        @include background-icon($icon, inactive, $big-icon-size);
-                    }
-                };
-
-                &.quarter {
-                    min-height: $large-icon-size-px;
-                    min-width: $large-icon-size-px;
-                    background-size: $large-icon-size-px;
-                }
-            }
-
-            .cw-block-headline-textbox {
-                margin: 2em 1em 2em 0;
-                .cw-block-headline-title {
-                    h1 {
-                        border: none;
-                        font-size: 5em;
-                        text-align: left;
-                    }
-                }
-
-                .cw-block-headline-subtitle {
-                    display: none;
-                }
-            }
-        }
-
-        &.ribbon {
-            display: flex;
-            align-items: center;
-
-            .cw-block-headline-textbox {
-                width: 100%;
-                padding: 1em 0;
-                margin: 3em 0;
-                background-color: fade-out($black, 0.5);
-                .cw-block-headline-title {
-                    h1 {
-                        border: none;
-                        font-size: 5em;
-                        text-align: center;
-                        margin-bottom: 0;
-                    }
-                }
-
-                .cw-block-headline-subtitle {
-                    h2 {
-                        border: none;
-                        font-size: 18px;
-                        text-align: center;
-                        margin-top: 10px;
-                    }
-                }
-            }
-        }
-
-        &.vertical {
-            .cw-block-headline-textbox {
-
-                .cw-block-headline-title {
-                    position: relative;
-                    top: 0;
-                    left: 5em;
-                    height: 5em;
-                    text-align: center;
-                    width: 600px;
-                    transform-origin: 0 0;
-                    transform: rotate(90deg);
-                    h1 {
-                        text-transform: uppercase;
-                        border: none;
-                        font-size: 2em;
-                        line-height: 2.5em;
-                    }
-                }
-
-                .cw-block-headline-subtitle {
-                    margin: 0 8em;
-                    padding: 2em;
-                    h2 {
-                        margin: 0;
-                        font-weight: 400;
-                    }
-                }
-            }
-            &.half {
-                .cw-block-headline-title {
-                    width: 300px;
-                }
-            }
-            &.quarter {
-                .cw-block-headline-title {
-                    width: 150px;
-                    h1 {
-                        font-size: 1em;
-                        line-height: 5em;
-                    }
-                }
-                .cw-block-headline-subtitle {
-                    margin: -2em 8em 0 8em;
-                    padding: 1em;
-                }
-            }
-        }
-
-        &.skew_text {
-            min-height: 600px;
-
-            .cw-block-headline-textbox {
-                font-size: 7em;
-                text-align: left;
-                text-transform: uppercase;
-                font-weight: 800;
-                font-family: sans-serif;
-                font-style: normal;
-                letter-spacing: -6px;
-                margin: 1.25em 0 1.5em 1em;
-                div {
-                    overflow: hidden;
-                    font-size: 1em;
-                    margin: -1px 0;
-                    white-space: nowrap;
-                    height: .8em;
-                    width: calc(100% - 1em);
-
-                    h1, h2 {
-                        font-size: 0.8em;
-                        margin: 0;
-                        white-space: nowrap;
-                        overflow: hidden;
-                        line-height: .8em;
-                        height: .8em;
-                    }
-                }
-                .cw-block-headline-title,
-                .cw-block-headline-second-subtitle {
-                    transform: skew(60deg,-15deg) scaley(.66667);
-                }
-                .cw-block-headline-subtitle {
-                    transform: skew(0deg,-15deg) scaley(1.33333);
-                }
-                .cw-block-headline-subtitle {
-                    position: relative;
-	                left: 0.48em;
-                }
-                .cw-block-headline-second-subtitle {
-                    position: relative;
-	                left: 0.92em;
-                }
-            }
-
-            &.half {
-                min-height: 300px;
-                height: 300px;
-                .cw-block-headline-textbox {
-                    font-size: 4em;
-                    margin: 1.5em 0 1.5em 1em;
-                    div {
-                        width: calc(100% - 9em);
-                    }
-                }
-            }
-            &.quarter {
-                min-height: 150px;
-                height: 150px;
-                .cw-block-headline-textbox {
-                    font-size: 2em;
-                    margin: 0.75em 0 0.75em 1em;
-                    letter-spacing: -2px;
-                    div {
-                        width: calc(100% - 18em);
-                    }
-                }
-            }
-        }
-
-        &.icon_top_lines {
-            .cw-block-headline-iconbox {
-                margin: 32px;
-                &::before,
-                &::after {
-                    width: calc(50% - 96px);
-                    height: 64px;
-                    display: inline-block;
-                    border-top: solid 4px;
-                    content: '';
-                }
-                @each $name, $hex in $border-colors {
-                    &.border-#{""+$name}::before,
-                    &.border-#{""+$name}::after {
-                        border-color: $hex;
-                    }
-                }
-                .icon-layer {
-                    background-repeat: no-repeat;
-                    background-position: center;
-                    background-size: 128px;
-                    height: 128px;
-                    width: 192px;
-                    display: inline-block;
-                    margin: auto;
-    
-                    @each $icon in $icons {
-                        &.icon-black-#{$icon} {
-                            @include background-icon($icon, info, 128);
-                        }
-                        &.icon-white-#{$icon} {
-                            @include background-icon($icon, info-alt, 128);
-                        }
-                        &.icon-studip-blue-#{$icon} {
-                            @include background-icon($icon, clickable, 128);
-                        }
-                        &.icon-studip-red-#{$icon} {
-                            @include background-icon($icon, status-red, 128);
-                        }
-                        &.icon-studip-yellow-#{$icon} {
-                            @include background-icon($icon, status-yellow, 128);
-                        }
-                        &.icon-studip-green-#{$icon} {
-                            @include background-icon($icon, status-green, 128);
-                        }
-                        &.icon-studip-gray-#{$icon} {
-                            @include background-icon($icon, inactive, 128);
-                        }
-                    };
-    
-                   &.half {
-                        height: 128px;
-                        background-size: 128px;
-                    }
-                    &.quarter {
-                        height: 64px;
-                        width: 96px;
-                        background-size: 64px;
-                    }
-                }
-                &.half {
-                    margin: 16px 32px;
-                }
-                &.quarter {
-                    margin: 8px 32px 0 32px;
-                    &::before,
-                    &::after {
-                        width: calc(50% - 48px);
-                        height: 32px;
-                    }
-                }
-            }
-            .cw-block-headline-textbox {
-                width: calc(100% - 64px);
-                min-height: 350px;
-                margin: 0 auto;
-                border-bottom: solid 4px;
-                @each $name, $hex in $border-colors {
-                    &.border-#{""+$name} {
-                        border-color: $hex;
-                    }
-                }
-                .cw-block-headline-title {
-                    h1 {
-                        border: none;
-                        font-size: 5em;
-                        text-align: center;
-                    }
-                }
-
-                .cw-block-headline-subtitle {
-                    h2 {
-                        border: none;
-                        font-size: 18px;
-                        text-align: center;
-                        margin: 22px 64px;
-                    }
-                }
-
-                &.half {
-                    min-height: 0px;
-                    margin-bottom: 32px;
-                    .cw-block-headline-title {
-                        h1 {
-                            font-size: 4em;
-                        }
-                    }
-    
-                    .cw-block-headline-subtitle {
-                        h2 {
-                            font-size: 1em;
-                            margin: 1em 64px;
-                        }
-                    }
-                }
-                &.quarter {
-                    min-height: 0;
-                    margin-bottom: 0;
-                    border: none;
-                    .cw-block-headline-title {
-                        h1 {
-                            font-size: 2em;
-                        }
-                    }
-    
-                    .cw-block-headline-subtitle {
-                        h2 {
-                            font-size: 1em;
-                            margin: 8px 64px;
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-.cw-container-colspan-half,
-.cw-container-colspan-half-center {
-    .cw-block-headline {
-        .cw-block-headline-content {
-            min-height: 300px;
-
-            &.half {
-                min-height: 150px;
-            }
-            &.heavy {
-                h1 {
-                    font-size: 4.5em;
-                }
-                h2 {
-                    font-size: 1.25em;
-                }
-            }
-            &.bigicon_top {
-                .icon-layer {
-                    background-position: center;
-                    height: 98px;
-                    margin-top: 2em;
-                    margin-bottom: 1em;
-
-                    @each $icon in $icons {
-                        &.icon-black-#{$icon} {
-                            @include background-icon($icon, info, 98);
-                        }
-                        &.icon-white-#{$icon} {
-                            @include background-icon($icon, info-alt, 98);
-                        }
-                        &.icon-studip-blue-#{$icon} {
-                            @include background-icon($icon, clickable, 98);
-                        }
-                        &.icon-studip-red-#{$icon} {
-                            @include background-icon($icon, status-red, 98);
-                        }
-                        &.icon-studip-yellow-#{$icon} {
-                            @include background-icon($icon, status-yellow, 98);
-                        }
-                        &.icon-studip-green-#{$icon} {
-                            @include background-icon($icon, status-green, 98);
-                        }
-                        &.icon-studip-gray-#{$icon} {
-                            @include background-icon($icon, inactive, 98);
-                        }
-                    };
-
-                    &.half {
-                        background-size: 72px;
-                        height: 72px;
-                        background-position: center;
-                    }
-                }
-
-
-                .cw-block-headline-textbox {
-                    max-width: 80%;
-                    margin: 0 auto;
-                    padding-bottom: 1em;
-                    .cw-block-headline-title {
-                        h1 {
-                            font-size: 2em;
-                        }
-                    }
-
-                    .cw-block-headline-subtitle {
-                        h2 {
-                            font-size: 12px;
-                        }
-                    }
-                }
-            }
-            &.bigicon_before {
-                min-height: 300px;
-                .icon-layer {
-                    min-height: 92px;
-                    min-width: 92px;
-                    background-position: 0 center;
-                    @each $icon in $icons {
-                        &.icon-black-#{$icon} {
-                            @include background-icon($icon, info, 92);
-                        }
-                        &.icon-white-#{$icon} {
-                            @include background-icon($icon, info-alt, 92);
-                        }
-                        &.icon-studip-blue-#{$icon} {
-                            @include background-icon($icon, clickable, 92);
-                        }
-                        &.icon-studip-red-#{$icon} {
-                            @include background-icon($icon, status-red, 92);
-                        }
-                        &.icon-studip-yellow-#{$icon} {
-                            @include background-icon($icon, status-yellow, 92);
-                        }
-                        &.icon-studip-green-#{$icon} {
-                            @include background-icon($icon, status-green, 92);
-                        }
-                        &.icon-studip-gray-#{$icon} {
-                            @include background-icon($icon, inactive, 92);
-                        }
-                    };
-                }
-
-                .cw-block-headline-textbox {
-                    .cw-block-headline-title {
-                        h1 {
-                            font-size: 2.5em;
-                        }
-                    }
-                }
-
-                &.half {
-                    min-height: 150px;
-                }
-            }
-
-            &.ribbon {
-                min-height: 300px;
-
-                &.half {
-                    min-height: 150px;
-                }
-
-                .cw-block-headline-textbox {
-                    margin: 3em 0;
-
-                    .cw-block-headline-title {
-                        h1 {
-                            font-size: 2.5em;
-                        }
-                    }
-                    .cw-block-headline-subtitle {
-                        h2 {
-                            font-size: 12px;
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-
-.responsive-display {
-    .cw-block-headline {
-        .cw-block-headline-content {
-            background-size: 100%;
-            &.bigicon_before {
-                .icon-layer {
-                    background-size: 144px;
-                }
-                .cw-block-headline-textbox .cw-block-headline-title h1 {
-                    font-size: 4em;
-                }
-            }
-            &.skew_text {
-                min-height: 300px;
-                .cw-block-headline-textbox {
-                    font-size: 4em;
-                    letter-spacing: -4px;
-                }
-            }
-        }
-    }
-}
-
-/*
-headline block end
-*/
-
-/*
-toc block
-*/
-.cw-block-table-of-contents {
-    .cw-block-content {
-        overflow: unset;
-    }
-}
-
-.cw-block-table-of-contents-list {
-    padding: 0;
-    list-style: none;
-    border: solid thin var(--content-color-40);
-    li {
-        &:not(:last-child) {
-            border-bottom: solid thin var(--dark-gray-color-30);
-        }
-        a {
-            display: block;
-            padding: 1em;
-        }
-        &:hover {
-            background-color: fade-out($dark-gray-color-75, 0.8);
-        }
-    }
-}
-
-.cw-block-table-of-contents-list-details {
-    padding: 0;
-    list-style: none;
-    border: solid thin var(--content-color-40);
-    li {
-        &:not(:last-child) {
-            border-bottom: solid thin var(--dark-gray-color-30);
-        }
-
-        &:hover {
-            background-color: fade-out($dark-gray-color-75, 0.8);
-        }
-        a {
-            display: block;
-            padding: 1em;
-        }
-    }
-}
-
-.cw-block-table-of-contents-title-box {
-    min-height: 3em;
-    padding-left: 1em;
-    border-left-width: 10px;
-    border-left-style: solid;
-
-    @each $name, $color in $tile-colors {
-        &.#{"" + $name} {
-            border-color: $color;
-        }
-    };
-
-    p, p:hover {
-        color: var(--black);
-    }
-}
-
-.cw-container-colspan-half {
-    .cw-block-table-of-contents-tiles.cw-tiles  {
-        .tile {
-            width: 267px;
-        }
-    }
-}
-
-/*
-toc block end
-*/
-
-/*
-text block
-*/
-.cw-block-text {
-    .cktoolbar {
-        width: 100% !important;
-        max-width: 100% !important;
-        position: relative !important;
-        top: 0 !important;
-    }
-    .cke {
-        width: 100% !important;
-    }
-    .ckplaceholder {
-        height: 0 !important;
-    }
-}
-/*
-text block end
-*/
-
-/* * * * * * * *
- t i m e l i n e
-* * * * * * * */
-// Mixins and Placeholders
-%clearfix {
-    &:after, &:before {
-        content: '';
-        display: block;
-        width: 100%;
-        clear: both;
-    }
-}
-
-// Timeline
-.cw-timeline {
-    list-style: none;
-    width: 100%;
-    margin: 30px auto;
-    position: relative;
-    padding: 0;
-    transition: all 0.4s ease;
-
-    &:before {
-        content:"";
-        width: 3px;
-        height: 100%;
-        background: var(--content-color-40);
-        left: 50%;
-        top: 0;
-        position: absolute;
-    }
-
-    &:after {
-        content: "";
-        clear: both;
-        display: table;
-        width: 100%;
-    }
-
-    .cw-timeline-item {
-        margin-bottom: 50px;
-        position: relative;
-        @extend %clearfix;
-
-        .cw-timeline-item-icon {
-            background: var(--white);
-            width: 50px;
-            height: 50px;
-            position: absolute;
-            top: 0;
-            left: 50%;
-            overflow: hidden;
-            margin-left: -25px;
-            @each $name, $color in $tile-colors {
-                &.cw-timeline-item-icon-color-#{"" + $name} {
-                    border: solid 2px $color;
-                }
-            }
-
-            border-radius: 50%;
-
-            img {
-                position: relative;
-                top: 9px;
-                left: 9px;
-
-                &.charcoal {
-                    filter: brightness(0) saturate(100%) invert(22%) sepia(29%) saturate(364%) hue-rotate(168deg) brightness(87%) contrast(79%);
-                }
-                &.royal-purple {
-                    filter: brightness(0) saturate(100%) invert(35%) sepia(43%) saturate(658%) hue-rotate(234deg) brightness(100%) contrast(87%);
-                }
-                &.iguana-green {
-                    filter: brightness(0) saturate(100%) invert(74%) sepia(9%) saturate(1885%) hue-rotate(76deg) brightness(86%) contrast(88%);
-                }
-                &.queen-blue {
-                    filter: brightness(0) saturate(100%) invert(44%) sepia(10%) saturate(2086%) hue-rotate(178deg) brightness(88%) contrast(80%);
-                }
-                &.verdigris {
-                    filter: brightness(0) saturate(100%) invert(64%) sepia(11%) saturate(4959%) hue-rotate(131deg) brightness(103%) contrast(49%);
-                }
-                &.mulberry {
-                    filter: brightness(0) saturate(100%) invert(49%) sepia(16%) saturate(1665%) hue-rotate(271deg) brightness(88%) contrast(95%);
-                }
-                &.pumpkin {
-                    filter: brightness(0) saturate(100%) invert(38%) sepia(86%) saturate(1993%) hue-rotate(13deg) brightness(104%) contrast(108%);
-                }
-                &.sunglow {
-                    filter: brightness(0) saturate(100%) invert(93%) sepia(69%) saturate(6824%) hue-rotate(313deg) brightness(102%) contrast(100%);
-                }
-                &.apple-green {
-                    filter: brightness(0) saturate(100%) invert(69%) sepia(5%) saturate(5203%) hue-rotate(42deg) brightness(100%) contrast(84%);
-                }
-                &.studip-blue {
-                    filter: brightness(0) saturate(100%) invert(26%) sepia(19%) saturate(1783%) hue-rotate(177deg) brightness(96%) contrast(93%);
-                }
-                &.studip-lightblue {
-                    filter: brightness(0) saturate(100%) invert(91%) sepia(12%) saturate(190%) hue-rotate(190deg) brightness(104%) contrast(89%);
-                }
-                &.studip-red {
-                    filter: brightness(0) saturate(100%) invert(8%) sepia(95%) saturate(6904%) hue-rotate(1deg) brightness(95%) contrast(109%);
-                }
-                &.studip-green {
-                    filter: brightness(0) saturate(100%) invert(27%) sepia(85%) saturate(1531%) hue-rotate(109deg) brightness(95%) contrast(101%);
-                }
-                &.studip-yellow {
-                    filter: brightness(0) saturate(100%) invert(94%) sepia(14%) saturate(7314%) hue-rotate(330deg) brightness(103%) contrast(101%);
-                }
-                &.studip-gray {
-                    filter: brightness(0) saturate(100%) invert(46%) sepia(1%) saturate(2621%) hue-rotate(169deg) brightness(87%) contrast(87%);
-                }
-            }
-        }
-
-        .cw-timeline-item-content {
-            width: 40%;
-            background: var(--white);
-            padding: 20px;
-            transition: all var(--transition-duration) ease;
-
-            h3 {
-                padding: 15px;
-                color: var(--white);
-                margin: -20px -20px 0 -20px;
-                font-weight: 700;
-                min-height: 1.1em;
-            }
-
-            article {
-                min-height: 2em;
-                border: solid thin var(--content-color-20);
-                border-top: none;
-                margin: 0 -20px;
-                padding: 15px;
-
-                header {
-                    font-weight: 700;
-                    font-size: 1.1em;
-                    margin: 0.5em 0;
-                }
-            }
-
-            &:before {
-                content: '';
-                position: absolute;
-                left: calc(40% + 40px);
-                top: 18px;
-                width: 0;
-                height: 0;
-                border-top: 7px solid transparent;
-                border-bottom: 7px solid transparent;
-            }
-            &.left {
-                h3 {
-                    text-align: right;
-                }
-            }
-            &.right {
-                float: right;
-
-                h3 {
-                    text-align: left;
-                }
-
-                &:before {
-                    content: '';
-                    right: calc(40% + 40px);
-                    left: inherit;
-                    border-left: 0;
-                }
-            }
-
-            @each $name, $color in $tile-colors {
-                &.cw-timeline-item-content-color-#{"" + $name} {
-                    border-color: var(--color);
-                    h3 {
-                        background-color: var(--color);
-                    }
-                    &.left {
-                        &:before {
-                            border-left: 7px solid var(--color);
-                        }
-                    }
-                    &.right {
-                        &:before {
-                            border-right: 7px solid var(--color);
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-@mixin oneSidedTimeline() {
-    .cw-timeline {
-        &:before {
-            left: 25px;
-        }
-        .cw-timeline-item {
-            .cw-timeline-item-icon {
-                left: 25px;
-            }
-            .cw-timeline-item-content {
-                width: stretch;
-                margin-left: 70px;
-                &.left {
-                    float: unset;
-
-                    h3 {
-                        text-align: left;
-                    }
-
-                    &:before {
-                        content: '';
-                        right: calc(100% - 70px);
-                        left: inherit;
-                        border-left: 0;
-                    }
-                }
-                &.right {
-                    float: unset;
-                    &:before {
-                        right: calc(100% - 70px);
-                    }
-                }
-
-                @each $name, $color in $tile-colors {
-                    &.cw-timeline-item-content-color-#{"" + $name} {
-                        &.left {
-                            &:before {
-                                border-right: 7px solid $color;
-                                border-left: none;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-.cw-container-colspan-half {
-    @include oneSidedTimeline();
-}
-
-@media only screen and (max-width: 1070px) {
-    @include oneSidedTimeline();
-}
-/* * * * * * * * * * * *
- t i m e l i n e  e n d
-* * * * * * * * * * * */
-
-/*
-cw tiles
-*/
-
-.cw-tiles {
-    list-style: none;
-    display: flex;
-    flex-wrap: wrap;
-    padding-left: 0;
-    row-gap: 5px;
-    column-gap: 5px;
-}
-.cw-tiles .tile,
-.cw-tile {
-    height: 420px;
-    width: 270px;
-    margin: 0;
-    background-color: var(--base-color);
-    &:last-child {
-        margin-right: 0;
-    }
-
-    @each $name, $color in $tile-colors {
-        &.#{"" + $name} {
-            background-color: $color;
-        }
-    };
-
-    .preview-image {
-        height: 180px;
-        width: 100%;
-        background-size: 100% auto;
-        background-repeat: no-repeat;
-        background-color: var(--content-color-20);
-        background-position: center;
-        &.default-image {
-                @include background-icon(courseware, clickable, 128);
-        }
-
-        .overlay-text {
-            padding: 6px 7px;
-            margin: 4px;
-            background-color: rgba(255,255,255,0.8);
-            width: fit-content;
-            max-width: 100%;
-            height: 1.25em;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            float: right;
-            text-align: right;
-        }
-
-        .overlay-action-menu {
-            padding: 0;
-            margin: 0.25em;
-            background-color: rgba(255,255,255,0.8);
-            width: fit-content;
-            max-width: 100%;
-            overflow: hidden;
-            float: right;
-            text-align: right;
-            .action-menu {
-                margin: 5px;
-            }
-        }
-    }
-
-    .description {
-        height: 220px;
-        padding: 14px;
-        color: var(--white);
-        position: relative;
-        display: block;
-
-        header {
-            font-size: 20px;
-            line-height: 22px;
-            color: var(--white);
-            border: none;
-            width: 240px;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            white-space: nowrap;
-            background-repeat: no-repeat;
-            background-position: 0 0;
-
-            @each $type, $icon in $element-icons {
-                &.description-icon-#{$type} {
-                    width: 212px;
-                    padding-left: 28px;
-                    @include background-icon(#{$icon}, info_alt, 22);
-                }
-            }
-        }
-
-        .progress-wrapper {
-            width: 100%;
-            padding: 1em 0;
-            border: none;
-            background: none;
-
-            progress {
-                appearance: none;
-                display: block;
-                width: 100%;
-                height: 3px;
-                margin: 0;
-                border: none;
-                background: rgba(0,0,0,0.3);
-                &::-webkit-progress-bar {
-                    background:  rgba(0,0,0,0.3);
-                }
-                &::-webkit-progress-value {
-                    background: white;
-                }
-                &::-moz-progress-bar {
-                    background: white;
-                }
-            }
-        }
-
-        .description-text-wrapper {
-            overflow: hidden;
-            height: 10em;
-            margin-top: 0.5em;
-            display: -webkit-box;
-            margin-bottom: 1em;
-            -webkit-line-clamp: 7;
-            -webkit-box-orient: vertical;
-            p {
-                text-align: left;
-            }
-        }
-
-        footer {
-            width: 242px;
-            text-align: right;
-            color: var(--white);
-            white-space: nowrap;
-            overflow: hidden;
-            text-overflow: ellipsis;
-
-            img {
-                vertical-align: text-bottom;
-            }
-        }
-    }
-
-    a[href].description {
-        transition: unset;
-    }
-
-    a.description,
-    a.description:link,
-    a.description:visited,
-    a.description:hover {
-        height: 210px;
-        color: var(--white);
-        text-decoration: unset;
-    }
-}
-/*
-cw tiles end
-*/
-
-/* courseware template preview */
-.cw-template-preview {
-    display: flex;
-    flex-wrap: wrap;
-    justify-content: space-between;
-    width: calc(100% - 20px);;
-    padding: 10px;
-    .cw-template-preview-container-wrapper {
-        margin-bottom: 10px;
-
-        &.cw-template-preview-container-full {
-            width: 100%
-        }
-        &.cw-template-preview-container-half {
-            width: calc(50% - 4px);
-        }
-        &.cw-template-preview-container-half-center {
-            width: 100%;
-            .cw-template-preview-container-content {
-                width: 50%;
-                margin: auto;
-            }
-        }
-
-        .cw-template-preview-container-content {
-            border: solid thin var(--content-color-40);
-        }
-
-        .cw-template-preview-container-title {
-            font-weight: 700;
-            padding: 4px 4px 4px 8px;
-            color: var(--base-color);
-            background-color: var(--content-color-20);
-        }
-
-        .cw-template-preview-blocks {
-            border: solid thin var(--content-color-40);
-            padding: 1em;
-            margin: 5px;
-            background-color: var(--white);
-
-        }
-    }
-}
-/* courseware template preview end*/
-
-/* * * * * * * * * *
- i n p u t  f i l e
-* * * * * * * * * */
- .cw-file-input {
-     width: stretch;
-     border: solid thin var(--base-color);
-     font-size: 14px;
-    cursor: pointer;
-
-    &::file-selector-button {
-        font-family: inherit;
-        border: none;
-        border-right: solid thin var(--base-color);
-        background-color: var(--white);
-        padding: 6px 15px;
-        margin-right: 10px;
-        color: var(--base-color);
-
-        &:hover {
-            background-color: var(--base-color);
-            color: var(--white);
-        }
-     }
- }
- .cw-file-input-change {
-    border: solid thin var(--base-color);
-
-    button.button {
-        padding: 0.5em 1.5em;
-        margin: 0 0 0 -1px;
-        line-height: 100%;
-        border: none;
-        border-right: solid thin var(--base-color);
-    }
-    span {
-        padding: 0.5em 1.5em 0.5em 0.5em;
-    }
- }
- /* * * * * * * * * * * * *
- i n p u t  f i l e  e n d
-* * * * * * * * * * * * * */
-
-/* * * * * * * * * * * *
-p u b l i c  l i n k s
-* * * * * * * * * * * */
-.cw-public-link-clipboard-button {
-    width: 16px;
-    height: 16px;
-    margin-left: 0.5em;
-    background-color: transparent;
-    border: none;
-    vertical-align: text-bottom;
-    @include background-icon(clipboard, clickable, 16);
-    cursor: pointer;
-
-}
-/* * * * * * * * * * * * * * *
-e n d  p u b l i c  l i n k s
-* * * * * * * * * * * * * * */
-
-/* * * * * * * * * * * *
-a s s i s t i v e
-* * * * * * * * * * * */
-.assistive-text {
-    position: absolute;
-    margin: -1px;
-    border: 0;
-    padding: 0;
-    width: 1px;
-    height: 1px;
-    overflow: hidden;
-    clip: rect(0 0 0 0);
-  }
-/* * * * * * * * * * * * * * *
-e n d  a s s i s t i v e
-* * * * * * * * * * * * * * */
-
-/* * * * * * * * * * * * * * *
-w i z a r d  e l e m e n t s
-* * * * * * * * * * * * * * */
-.cw-element-selector-list {
-    list-style: none;
-    padding: 0;
-
-    .cw-element-selector-item {
-        display: block;
-        width: 100%;
-        border: solid thin var(--content-color-40);
-        padding: 0.5em;
-        margin-bottom: 5px;
-        background-color: var(--white);
-        color: var(--base-color);
-        text-align: left;
-        cursor: pointer;
-
-        &:hover {
-            color: var(--white);
-            background-color: var(--base-color);
-        }
-    }
-}
-form.default .courseware-structural-element-selector {
-    list-style: none;
-    padding-left: 0;
-    .courseware-structural-element-selector-item {
-        .radiobutton {
-            background: none;
-            border: none;
-            padding: 0;
-        }
-        a label {
-            cursor: pointer;
-        }
-        label {
-            display: inline-block;
-            margin-bottom: 0;
-            text-indent: 0;
-            vertical-align: middle;
-        }
-        img {
-            vertical-align: middle;
-            &.inactive {
-                opacity: 0.5;
-            }
-        }
-        ul {
-            list-style: none;
-            padding-left: 18px;
-        }
-    }
-}
-/* * * * * * * * * * * * * * * * * *
-w i z a r d  e l e m e n t s  e n d
-* * * * * * * * * * * * * * * * * */
-
-
-/* * * * * * * * * * * * * * *
-s h e l f  i t e m s
-* * * * * * * * * * * * * * */
-.cw-unit-items,
-.cw-shared-items {
-    margin-bottom: 10px;
-    h2 {
-        margin-top: 0;
-    }
-}
-/* * * * * * * * * * * * * * * * * *
-s h e l f  i t e m s  e n d
-* * * * * * * * * * * * * * * * * */
-
-/* * * * * * * * *
-r a d i o s e t
-* * * * * * * * */
-
-.cw-radioset {
-    display: flex;
-    flex-direction: row;
-    justify-content: center;
-    margin-bottom: 1em;
-    .cw-radioset-box {
-        width: 128px;
-        height: 128px;
-        text-align: center;
-        margin-right: 16px;
-        border: solid thin var(--content-color-40);
-        &.selected {
-            border-color: var(--base-color);
-            background-color: var(--content-color-20);
-        }
-        &:last-child {
-            margin-right: 0;
-        }
-        label {
-            height: 100%;
-            width: 100%;
-            margin: 0;
-            cursor: pointer;
-            .label-icon {
-                background-position: center 8px;
-                background-repeat: no-repeat;
-                height: 64px;
-                padding: 8px;
-                &.accordion {
-                    @include background-icon(block-accordion, clickable, 64);
-                }
-                &.list {
-                    @include background-icon(view-list, clickable, 64);
-                }
-                &.tabs {
-                    @include background-icon(block-tabs, clickable, 64);
-                }
-                &.full {
-                    @include background-icon(column-full, clickable, 64);
-                }
-                &.half {
-                    @include background-icon(column-half, clickable, 64);
-                }
-                &.half-center {
-                    @include background-icon(column-half-centered, clickable, 64);
-                }
-            }
-
-        }
-        input[type=radio] {
-            position: absolute;
-            opacity: 0;
-            width: 0;
-
-            &:focus + label {
-                outline-color: Highlight;
-                outline-color: -webkit-focus-ring-color;
-                outline-style: auto;
-                outline-width: 1px;
-            }
-        }
-    }
-}
-
-/* * * * * * * * * * * *
-r a d i o s e t  e n d 
-* * * * * * * * * * * */
-/* * * * * * * * * * * * * * * *
-c o m m e n t s  o v e r v i e w
-* * * * * * * * * * * * * * * */
-
-.cw-comments-overview-dialog-comments-context {
-    margin: 0 0 1.5em 0;
-}
-
-/* * * * * * * * * * * * * * * * * * * *
-c o m m e n t s  o v e r v i e w  e n d
-* * * * * * * * * * * * * * * * * * * */
-
-#course-courseware-index,
-#contents-courseware-index,
-#course-courseware-courseware,
-#contents-courseware-courseware {
-    form.default input[type=date] {
-        max-width: 9em;
-    }
-}
+@import './courseware/variables.scss';
+
+@import './courseware/a11y.scss';
+@import './courseware/blockadder.scss';
+@import './courseware/comments.scss';
+@import './courseware/content-courses.scss';
+@import './courseware/dashboard.scss';
+@import './courseware/sortable.scss';
+@import './courseware/widgets.scss';
+@import './courseware/wizards.scss';
+
+@import './courseware/shelf.scss';
+@import './courseware/structural-element.scss';
+@import './courseware/containers/default-container.scss';
+@import './courseware/blocks/default-block.scss';
+
+@import './courseware/layouts/collapsible.scss';
+@import './courseware/layouts/companion.scss';
+@import './courseware/layouts/import-zip.scss';
+@import './courseware/layouts/input-file.scss';
+@import './courseware/layouts/progress.scss';
+@import './courseware/layouts/ribbon.scss';
+@import './courseware/layouts/tabs.scss';
+@import './courseware/layouts/talk-bubble.scss';
+@import './courseware/layouts/tile.scss';
+@import './courseware/layouts/tree.scss';
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/a11y.scss b/resources/assets/stylesheets/scss/courseware/a11y.scss
new file mode 100644
index 0000000..b887e74
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/a11y.scss
@@ -0,0 +1,10 @@
+.assistive-text {
+    position: absolute;
+    margin: -1px;
+    border: 0;
+    padding: 0;
+    width: 1px;
+    height: 1px;
+    overflow: hidden;
+    clip: rect(0 0 0 0);
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blockadder.scss b/resources/assets/stylesheets/scss/courseware/blockadder.scss
new file mode 100644
index 0000000..12a43a9
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blockadder.scss
@@ -0,0 +1,319 @@
+.cw-tools-element-adder-tabs {
+    .cw-tabs-nav {
+        margin-top: 4px;
+        border: none;
+        border-bottom: solid thin var(--content-color-40);
+
+        button {
+            max-width: unset;
+            padding: 1em 1.5em 4px 1.5em;
+            margin: 0px 2em;
+        }
+    }
+    .cw-tabs-content {
+        border: none;
+        overflow-x: hidden;
+        overflow-y: auto;
+        scrollbar-width: thin;
+        scrollbar-color: var(--base-color) var(--dark-gray-color-5);
+
+        .input-group.files-search {
+            &.search {
+                border: thin solid var(--dark-gray-color-30);
+                margin-bottom: 0px;
+                input {
+                    border: none;
+                }
+            }
+
+            .input-group-append {
+                .button {
+                    border: none;
+                    border-left: thin solid var(--dark-gray-color-30);
+                    &.active {
+                        background-color: var(--base-color);
+                    }
+                }
+                .reset-search {
+                    border: none;
+                    background-color: var(--white);
+                }
+            }
+
+            .active-filter {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                border: solid thin var(--black);
+                background-color: var(--content-color-10);
+                margin: 3px;
+                padding: 2px 3px;
+
+                .removefilter {
+                    border: none;
+                    background-color: transparent;
+                }
+            }
+        }
+
+        .cw-block-search {
+            width: inherit;
+        }
+
+        .filterpanel {
+            margin-bottom: 5px;
+            padding: 2px;
+            border: thin solid var(--dark-gray-color-30);
+            border-top: none;
+            background-color: var(--white);
+
+            .button {
+                min-width: inherit;
+                margin: 4px 2px;
+
+                &.button-active {
+                    background-color: var(--base-color);
+                    color: var(--white);
+                }
+            }
+        }
+    }
+
+    .cw-collapsible {
+        .cw-collapsible-content {
+            display: none;
+            &.cw-collapsible-content-open {
+                display: block;
+            }
+        }
+    }
+}
+
+.cw-element-adder-wrapper {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+}
+.cw-blockadder-item-list {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+    grid-auto-rows: auto;
+    grid-gap: 4px;
+
+    .cw-blockadder-item-wrapper {
+        display: flex;
+        border: solid thin var(--content-color-40);
+        max-width: 254px;
+
+        &:hover {
+            border-color: var(--base-color);
+        }
+        .cw-blockadder-item {
+            padding: 64px 10px 4px 10px;
+            @include background-icon(unit-test, clickable, 48);
+            background-position: 10px 10px;
+            background-repeat: no-repeat;
+            cursor: pointer;
+
+            @each $item, $icon in $blockadder-items {
+                &.cw-blockadder-item-#{$item} {
+                    @include background-icon($icon, clickable, 48);
+                }
+            }
+            .cw-clipboard-item-title,
+            .cw-blockadder-item-title {
+                display: inline-block;
+                font-weight: 600;
+                margin-bottom: 2px;
+            }
+            .cw-blockadder-item-description {
+                display: inline-block;
+                margin: 0 0 4px;
+            }
+        }
+        .cw-blockadder-item-fav {
+            height: 32px;
+            padding: 8px;
+            background-color: transparent;
+            border: none;
+            cursor: pointer;
+        }
+    }
+}
+
+.cw-block-adder-area {
+    background-color: var(--white);
+    border: solid thin var(--content-color-40);
+    padding: 1em 0;
+    color: var(--base-color);
+    text-align: center;
+    width: 100%;
+    font-weight: 600;
+    cursor: pointer;
+
+    &:hover {
+        border-color: var(--base-color);
+    }
+
+    &.cw-block-adder-active {
+        border: solid thin var(--base-color);
+        background-color: var(--base-color);
+        color: var(--white);
+    }
+
+    img {
+        vertical-align: text-bottom;
+    }
+}
+.cw-block-helper-buttons {
+    display: inline-block;
+    width: 100%;
+
+    .cw-block-helper-reset {
+        float: right;
+    }
+
+    .button.cw-block-helper-reset::before {
+        content: '';
+        @include background-icon(refresh);
+        background-repeat: no-repeat;
+        float: left;
+        height: 16px;
+        width: 16px;
+        margin: 1px 5px 0 -8px;
+    }
+}
+
+.cw-block-helper-results {
+    margin-top: 5px;
+}
+
+.cw-containeradder-item {
+    margin-bottom: 4px;
+    padding: 1em 1em 1em 6em;
+    @include background-icon(unit-test, clickable, 48);
+    background-position: 12px center;
+    background-repeat: no-repeat;
+    border: solid thin var(--content-color-40);
+    cursor: pointer;
+
+    &:hover {
+        border-color: var(--base-color);
+    }
+
+    @each $item, $icon in $containeradder-items {
+        &.cw-containeradder-item-#{$item} {
+            @include background-icon($icon, clickable, 48);
+        }
+    }
+
+    .cw-containeradder-item-title {
+        font-weight: 600;
+    }
+}
+
+.cw-container-style-selector {
+    display: flex;
+    margin-bottom: 8px;
+
+    label {
+        border: solid thin var(--content-color-40);
+        padding: calc(0.5em + 32px) 1em 0.5em 1em;
+        color: var(--base-color);
+        text-align: center;
+        width: 33%;
+        background-position: center 0.5em;
+        background-repeat: no-repeat;
+        cursor: pointer;
+
+        &.full {
+            @include background-icon(column-full, clickable, 32);
+        }
+        &.half {
+            @include background-icon(column-half, clickable, 32);
+        }
+        &.half-center {
+            @include background-icon(column-half-centered, clickable, 32);
+        }
+        &:hover {
+            color: var(--active-color);
+        }
+        &:not(:first-child) {
+            border-left: solid thin transparent;
+        }
+        &.cw-container-style-selector-active {
+            background-color: var(--content-color-20);
+            border: solid thin var(--base-color);
+        }
+
+    }
+    input[type=radio] {
+        position: absolute;
+        opacity: 0;
+        width: 0;
+
+        &:focus + label {
+            outline-color: Highlight;
+            outline-color: -webkit-focus-ring-color;
+            outline-style: auto;
+            outline-width: 1px;
+        }
+    }
+}
+.cw-element-inserter-wrapper {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+}
+
+
+.cw-clipboard-item-wrapper {
+    display: flex;
+    width: calc(50% - 4px);
+    border: solid thin var(--content-color-40);
+    margin-bottom: 4px;
+
+    &:hover {
+        border-color: var(--base-color);
+    }
+
+    .cw-clipboard-item {
+        width: 207px;
+        padding: 64px 10px 4px 10px;
+        @include background-icon(unit-test, clickable, 48);
+        background-position: 10px 10px;
+        background-repeat: no-repeat;
+        cursor: pointer;
+        background-color: var(--white);
+        border: none;
+        text-align: left;
+        color: var(--base-color);
+
+        @each $item, $icon in $blockadder-items {
+            &.cw-clipboard-item-#{$item} {
+                @include background-icon($icon, clickable, 48);
+            }
+        }
+        @each $item, $icon in $containeradder-items {
+            &.cw-clipboard-item-#{$item} {
+                @include background-icon($icon, clickable, 48);
+            }
+        }
+    
+        .cw-clipboard-item-title {
+            display: inline-block;
+            font-weight: 600;
+            margin-bottom: 2px;
+        }
+
+    }
+    .cw-clipboard-item-action-menu-wrapper {
+        padding: 8px;
+    }
+}
+.action-menu.is-open,
+.action-menu-wrapper.is-open {
+    &.cw-clipboard-item-action-menu {
+        z-index: 42;
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/audio.scss b/resources/assets/stylesheets/scss/courseware/blocks/audio.scss
new file mode 100644
index 0000000..c5c530e
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/audio.scss
@@ -0,0 +1,231 @@
+@use '../../../mixins.scss' as *;
+
+$media-buttons: (
+    play: play,
+    stop: stop,
+    pause: pause,
+    prev: arr_eol-left,
+    next: arr_eol-right
+);
+
+.cw-block-audio {
+    .cw-audio-container {
+        border: solid thin var(--content-color-40);
+        padding-top: 1em;
+    }
+    .cw-audio-controls {
+        text-align: right;
+        padding: 0 0.5em;
+    }
+    .cw-audio-range {
+        margin: 0 5px 10px 0;
+        &::-moz-focus-outer {
+            border: 0;
+        }
+        &.ui-widget-content {
+            background-color: var(--base-color);
+        }
+        .ui-widget-header {
+            background-color: var(--dark-gray-color-5);
+        }
+        .ui-slider-handle {
+            border-radius: 20px;
+            width: 1em;
+            height: 1.7em;
+            top: -0.5em;
+            background-color: var(--dark-gray-color-20);
+            border-color: var(--content-color-40);
+            cursor: pointer;
+            margin-left: -2px;
+        }
+    }
+    .cw-audio-button {
+        border: solid thin var(--content-color-40);
+        background-color: var(--white);
+        background-repeat: no-repeat;
+        background-position: center center;
+        background-size: 24px;
+        min-height: 27px;
+        line-height: 130%;
+        padding: 5px 15px 5px 30px;
+        cursor: pointer;
+        font-size: 14px;
+        box-sizing: border-box;
+        text-align: center;
+        text-decoration: none;
+        vertical-align: bottom;
+        white-space: nowrap;
+        min-width: unset;
+        margin: 5px;
+        height: 46px;
+        width: 46px;
+        display: inline-block;
+
+        &:hover {
+            background-color: var(--base-color);
+        }
+
+        @each $button, $icon in $media-buttons {
+            &.cw-audio-#{$button}button {
+                @include background-icon($icon, clickable, 24);
+                &:hover {
+                    @include background-icon($icon, info-alt, 24);
+                }
+            }
+        }
+    }
+
+    .cw-audio-time {
+        position: relative;
+        top: -1em;
+        color: var(--base-gray);
+    }
+
+    .cw-audio-range {
+        display: block;
+        margin: 0 auto 1.5em;
+        -webkit-appearance: none;
+        position: relative;
+        overflow: hidden;
+        height: 18px;
+        width: 100%;
+        cursor: pointer;
+        border-radius: 0;
+    }
+
+    .cw-audio-range::-webkit-slider-runnable-track {
+        background: var(--dark-gray-color-20);
+    }
+
+    .cw-audio-range::-webkit-slider-thumb {
+        -webkit-appearance: none;
+        width: 9px; /* 1 */
+        height: 18px;
+        background: var(--white);
+        box-shadow: -100vw 0 0 100vw var(--base-color);
+        border: solid thin var(--content-color-40);
+    }
+
+    .cw-audio-range::-moz-range-track {
+        height: 18px;
+        background: var(--dark-gray-color-10);
+    }
+
+    .cw-audio-range::-moz-range-thumb {
+        background: var(--white);
+        height: 18px;
+        width: 9px;
+        border: solid thin var(--content-color-40);
+        border-radius: 0 !important;
+        box-shadow: -100vw 0 0 100vw var(--base-color);
+        box-sizing: border-box;
+    }
+
+    .cw-audio-range::-ms-fill-lower {
+        background: var(--base-color);
+    }
+
+    .cw-audio-range::-ms-thumb {
+        background: var(--white);
+        border: solid thin var(--content-color-40);
+        height: 18px;
+        width: 9px;
+        box-sizing: border-box;
+    }
+
+    .cw-audio-range::-ms-ticks-after {
+        display: none;
+    }
+
+    .cw-audio-range::-ms-ticks-before {
+        display: none;
+    }
+
+    .cw-audio-range::-ms-track {
+        background: var(--dark-gray-color-20);
+        color: transparent;
+        height: 18px;
+        border: none;
+    }
+
+    .cw-audio-range::-ms-tooltip {
+        display: none;
+    }
+    .cw-audio-playlist-wrapper {
+        margin-top: -1em;
+        padding-top: 1em;
+        border: solid thin var(--content-color-40);
+        border-top: none;
+
+        &.empty {
+            border: none;
+        }
+
+        .cw-audio-playlist {
+            padding-left: 0;
+            list-style: none;
+            cursor: pointer;
+
+            &.with-recorder {
+                border-bottom: solid thin var(--content-color-40);
+            }
+
+            li {
+                margin: 0 1em;
+                &:not(:last-child) {
+                    border-bottom: solid thin var(--dark-gray-color-30);
+                }
+
+                .cw-playlist-item {
+                    display: block;
+                    @include background-icon(file-audio2, clickable, 24);
+                    background-repeat: no-repeat;
+                    background-position: 1em center;
+
+                    margin: 1em 0;
+                    padding: 1em;
+                    padding-left: 4em;
+                    color: var(--base-color);
+                    &:hover {
+                        color: var(--active-color);
+                    }
+                    &.current-item {
+                        @include background-icon(play, clickable, 24);
+                        font-weight: 700;
+                        &.is-playing {
+                            @include background-icon(pause, clickable, 24);
+                        }
+                    }
+                }
+            }
+        }
+        .cw-audio-playlist-recorder {
+            padding: 1em;
+        }
+    }
+
+    .cw-audio-current-track {
+        @include background-icon(file-audio2, info, 96);
+        background-position: top center;
+        background-repeat: no-repeat;
+        width: 100%;
+        min-height: 140px;
+        margin: 1em 0 2em 0;
+        p {
+            text-align: center;
+            padding-top: 106px;
+        }
+    }
+    .cw-audio-empty {
+        @include background-icon(file, info, 96);
+        border: solid thin var(--content-color-40);
+        background-position: center 1em;
+        background-repeat: no-repeat;
+        min-height: 140px;
+        padding: 1em;
+        p {
+            text-align: center;
+            padding-top: 106px;
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/biography.scss b/resources/assets/stylesheets/scss/courseware/blocks/biography.scss
new file mode 100644
index 0000000..a40239b
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/biography.scss
@@ -0,0 +1,82 @@
+@use '../../../mixins.scss' as *;
+
+$achievement-types: (
+    certificate: file-text,
+    accreditation: vcard,
+    award: medal,
+    book: literature,
+    publication: news,
+    membership: group3,
+);
+
+$goals-types: (
+    personal: person2,
+    school: doctoral-cap,
+    academic: doctoral-cap,
+    professional: tools,
+);
+
+.cw-block-biography {
+    .cw-block-biography-content {
+        display: flex;
+        min-height: 200px;
+        flex-direction: row;
+        padding: 2em 2em 2em 1em;
+        border: 2px solid var(--base-color);
+
+        .cw-block-biography-type {
+            margin: auto 1em auto 0;
+            padding-top: 96px;
+            min-width: 192px;
+            max-width: 192px;
+            text-align: center;
+            background-repeat: no-repeat;
+            background-position: center top;
+        }
+
+        .cw-block-biography-details {
+            h2, h3 {
+                margin-top: 0;
+            }
+        }
+
+    }
+}
+
+.cw-block-biography-achievements {
+    @each $type, $icon in $achievement-types {
+        .cw-block-biography-achievements-type-#{$type} {
+            @include background-icon($icon, clickable, 96);
+        }
+    }
+}
+
+.cw-block-biography-goals {
+    @each $type, $icon in $goals-types {
+        .cw-block-biography-goals-type-#{$type} {
+            @include background-icon($icon, clickable, 96);
+        }
+    }
+}
+
+.cw-block-biography-personal-information {
+    .cw-block-biography-content {
+        min-height: 140px;
+
+        .cw-block-biography-personal-information-type {
+            @include background-icon(person2, clickable, 96);
+        }
+
+        .cw-block-biography-personal-information-details {
+            display: grid;
+            max-height: 7em;
+            grid-template-columns: max-content 1fr;
+            grid-gap: 5px 10px;
+
+            .preface {
+                grid-column-start: 1;
+                grid-column-end: 3;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/canvas.scss b/resources/assets/stylesheets/scss/courseware/blocks/canvas.scss
new file mode 100644
index 0000000..acfd935
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/canvas.scss
@@ -0,0 +1,145 @@
+@use '../../../mixins.scss' as *;
+
+.cw-block-canvas {
+    .cw-canvasblock-canvas {
+        max-width: 100%;
+        border: solid thin var(--content-color-40);
+    }
+
+    .cw-canvasblock-upload-message {
+        display: none;
+    }
+
+    .cw-canvasblock-original-img {
+        display: none;
+    }
+
+    .cw-canvasblock-tool-selected-text {
+        cursor: text;
+    }
+
+    h1.cw-canvasblock-description {
+        border-bottom: none;
+    }
+
+    .cw-canvasblock-toolbar {
+        border: solid thin var(--content-color-40);
+        border-bottom: none;
+
+        .cw-canvasblock-buttonset {
+            display: inline-block;
+            padding: 5px;
+            margin-right: 0.5em;
+
+            button {
+                cursor: pointer;
+                user-select: none;
+                border: solid thin var(--content-color-40);
+                height: 32px;
+                width: 32px;
+                background-color: var(--white);
+                background-position: center;
+                background-repeat: no-repeat;
+                background-size: 24px 24px;
+
+                &.cw-canvasblock-color {
+                    $colors: (
+                        white: var(--white),
+                        blue: #3498db,
+                        green: #2ecc71,
+                        purple: #9b59b6,
+                        red: #e74c3c,
+                        yellow: #fed330,
+                        orange: #f39c12,
+                        grey: #95a5a6,
+                        darkgrey: #34495e,
+                        black: var(--black),
+                    );
+
+                    @each $name, $color in $colors {
+                        &.#{'' + $name} {
+                            background-color: $color;
+                        }
+                    }
+
+                    &.selected-color {
+                        border: solid 2px var(--black);
+                    }
+                }
+
+                &.cw-canvasblock-reset {
+                    @include background-icon(refresh, clickable, 24);
+                }
+
+                &.cw-canvasblock-size {
+                    @include background-icon(stop, clickable);
+
+                    &.cw-canvasblock-size-small {
+                        background-size: 8px 7px;
+                    }
+                    &.cw-canvasblock-size-normal {
+                        background-size: 16px 14px;
+                    }
+                    &.cw-canvasblock-size-large {
+                        background-size: 22px 20px;
+                    }
+                    &.cw-canvasblock-size-huge {
+                        background-size: 26px 24px;
+                    }
+                    &.selected-size {
+                        border: solid 2px var(--black);
+                    }
+                }
+
+                &.cw-canvasblock-tool {
+                    &.cw-canvasblock-tool-pen {
+                        @include background-icon(comment, clickable);
+                    }
+
+                    &.cw-canvasblock-tool-text {
+                        vertical-align: top;
+                        font-size: 22px;
+                        color: var(--base-color);
+                        font-weight: 600;
+                    }
+
+                    &.selected-tool {
+                        border: solid 2px var(--black);
+                    }
+                }
+
+                &.cw-canvasblock-undo {
+                    @include background-icon(arr_2left, clickable, 24);
+                }
+
+                &.cw-canvasblock-download {
+                    @include background-icon(download, clickable, 24);
+                }
+
+                &.cw-canvasblock-store {
+                    @include background-icon(upload, clickable, 24);
+                }
+
+                &.cw-canvasblock-show-all {
+                    @include background-icon(group2, clickable, 24);
+
+                    &.selected-view {
+                        border: solid 2px var(--black);
+                    }
+                }
+
+                &.cw-canvasblock-show-own {
+                    @include background-icon(person, clickable, 24);
+
+                    &.selected-view {
+                        border: solid 2px var(--black);
+                    }
+                }
+            }
+        }
+    }
+
+    .cw-canvasblock-tool-selected-text {
+        cursor: text;
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/code.scss b/resources/assets/stylesheets/scss/courseware/blocks/code.scss
new file mode 100644
index 0000000..ac401cf
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/code.scss
@@ -0,0 +1,88 @@
+.cw-block-code {
+    pre {
+        margin-bottom: 0;
+    }
+
+    .hljs {
+        display: block;
+        overflow-x: auto;
+        padding: 0.5em;
+        background: var(--dark-gray-color-5);
+        color: black;
+        border: solid thin var(--content-color-40);
+    }
+
+    .hljs-comment,
+    .hljs-quote,
+    .hljs-variable {
+        color: var(--dark-green);
+    }
+
+    .hljs-keyword,
+    .hljs-selector-tag,
+    .hljs-selector-class,
+    .hljs-built_in,
+    .hljs-name,
+    .hljs-tag {
+        color: var(--base-color);
+        font-weight: 600;
+    }
+
+    .hljs-string,
+    .hljs-title,
+    .hljs-section,
+    .hljs-attribute,
+    .hljs-literal,
+    .hljs-template-tag,
+    .hljs-template-variable,
+    .hljs-type,
+    .hljs-addition {
+        color: var(--orange-80);
+        font-weight: 400;
+    }
+
+    .hljs-deletion,
+    .hljs-selector-attr,
+    .hljs-selector-pseudo,
+    .hljs-meta {
+        color: var(--petrol);
+        font-weight: 400;
+    }
+
+    .hljs-doctag {
+        color: var(--dark-gray-color-75);
+        font-weight: 400;
+    }
+
+    .hljs-attr {
+        color: var(--active-color);
+        font-weight: 400;
+    }
+
+    .hljs-symbol,
+    .hljs-bullet,
+    .hljs-link {
+        color: var(--petrol);
+        font-weight: 400;
+    }
+
+    .hljs-emphasis {
+        font-style: italic;
+        font-weight: 400;
+    }
+
+    .hljs-strong {
+        font-weight: 600;
+    }
+
+    .code-lang {
+        background: var(--dark-gray-color-5);
+        border: solid thin var(--content-color-40);
+        border-top: none;
+        padding: 5px 10px;
+        text-align: right;
+        color: var(--dark-gray-color-45);
+        font-family: monospace;
+        text-transform: full-width;
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/confirm.scss b/resources/assets/stylesheets/scss/courseware/blocks/confirm.scss
new file mode 100644
index 0000000..0c236fc
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/confirm.scss
@@ -0,0 +1,12 @@
+.cw-block-confirm {
+    .cw-block-confirm-content {
+        border: solid thin var(--content-color-40);
+        padding: 1em;
+        margin: 0;
+
+        input[type='checkbox'] {
+            margin-right: 2em;
+            vertical-align: bottom;
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/date.scss b/resources/assets/stylesheets/scss/courseware/blocks/date.scss
new file mode 100644
index 0000000..f2cde08
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/date.scss
@@ -0,0 +1,79 @@
+.cw-container-colspan-half,
+.cw-container-colspan-half-center {
+    .cw-block-date {
+        .cw-block-content {
+            font-size: 9px;
+        }
+    }
+}
+
+.cw-block-date {
+    .cw-date-countdown,
+    .cw-date-date {
+        margin: 0 auto;
+        width: max-content;
+    }
+
+    .cw-date-countdown {
+        .cw-date-countdown-digit {
+            display: inline-block;
+            margin-right: 4px;
+
+            .cw-date-countdown-number {
+                font-size: 6em;
+                line-height: 1.25em;
+                height: 1.25em;
+                padding: 3px 0.5em;
+                background: rgba(245, 245, 245, 1);
+                font-weight: 300;
+            }
+
+            .cw-date-countdown-label-sg,
+            .cw-date-countdown-label-pl {
+                padding: 5px;
+                font-size: 1.25em;
+                text-align: left;
+                text-transform: uppercase;
+            }
+        }
+    }
+
+    .cw-date-date {
+        .cw-date-date-space {
+            display: inline-block;
+            width: 2em;
+        }
+
+        .cw-date-date-digits {
+            display: inline-block;
+
+            .cw-date-date-number {
+                font-size: 5em;
+                line-height: 1.25em;
+                height: 1.25em;
+                padding: 0.25em;
+                background: rgba(245, 245, 245, 1);
+                font-weight: 300;
+            }
+        }
+    }
+}
+
+.responsive-display {
+    .cw-block-date {
+        .cw-block-content {
+            font-size: 9px;
+
+            .cw-date-countdown-number {
+                font-size: 3.5em;
+            }
+            .cw-date-countdown-label-sg,
+            .cw-date-countdown-label-pl {
+                font-size: 1em;
+            }
+            .cw-date-date-number {
+                font-size: 3em;
+            }
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss b/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss
new file mode 100644
index 0000000..24c5751
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss
@@ -0,0 +1,134 @@
+.cw-default-block {
+    display: flex;
+    flex-flow: row;
+    .cw-default-block-invisible-info {
+        img {
+            vertical-align: text-bottom;
+        }
+    }
+
+}
+
+.cw-block-header {
+    background-color: var(--content-color-20);
+    padding: 4px 10px 4px 22px;
+
+    .cw-block-header-toggle {
+        display: inline-block;
+        width: calc(100% - 50px);
+    }
+
+    span {
+        color: var(--base-color);
+        font-weight: 700;
+        line-height: 2em;
+        font-size: 1.1em;
+
+        &.cw-default-block-invisible-info,
+        &.cw-default-block-blocker-warning {
+            font-weight: 400;
+        }
+    }
+
+    img {
+        vertical-align: text-bottom;
+    }
+
+    .cw-block-actions {
+        position: relative;
+        float: right;
+        margin-top: 4px;
+        .is-open{
+            z-index: 30;
+        }
+    }
+}
+
+.cw-discuss-wrapper,
+.cw-block-features {
+    header{
+        background-color: var(--content-color-20);
+        color: var(--base-color);
+        font-weight: 600;
+        padding: 0.5em;
+    }
+
+    .cw-block-features-content{
+        margin: 1em;
+    }
+}
+
+.cw-discuss-wrapper {
+    flex-shrink: 3;
+    flex-grow: 2;
+    margin-left: 10px;
+}
+
+.cw-block-edit textarea {
+    width: -moz-available;
+    height: -moz-available;
+    min-height: 8em;
+    border: solid thin var(--content-color-40);
+    resize: none;
+}
+
+.cw-block-actions {
+    padding-left: 14px;
+}
+.cw-button-box {
+    float: right;
+}
+
+.cw-content-wrapper {
+    display: flex;
+    flex-flow: column;
+    width: 100%;
+    .cw-block-content {
+        overflow: auto;
+        position: relative;
+    }
+}
+.cw-content-wrapper-active {
+    border: solid thin var(--content-color-40);
+    .cw-block-content {
+        padding: $cw-wrapper-gap;
+    }
+}
+
+
+.cw-container-wrapper-discuss {
+    .cw-container-colspan-full {
+        .cw-content-wrapper {
+            max-width: $max-content-width;
+        }
+    }
+    .cw-container-colspan-half,
+    .cw-container-colspan-half-center {
+        .cw-content-wrapper {
+            max-width: 540px;
+        }
+    }
+}
+
+.cw-block-title {
+    padding: 4px;
+    background-color: var(--content-color-20);
+    color: var(--base-color);
+    font-weight: 700;
+    text-align: center;
+    border: solid thin var(--content-color-40);
+    border-bottom: none;
+}
+
+.cw-file-empty {
+    @include background-icon(file, info, 96);
+    border: solid thin var(--content-color-40);
+    background-position: center 1em;
+    background-repeat: no-repeat;
+    min-height: 140px;
+    padding: 1em;
+    p {
+        text-align: center;
+        padding-top: 106px;
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/dialog-cards.scss b/resources/assets/stylesheets/scss/courseware/blocks/dialog-cards.scss
new file mode 100644
index 0000000..e8ba467
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/dialog-cards.scss
@@ -0,0 +1,150 @@
+@use '../../../mixins.scss' as *;
+
+.cw-block-dialog-cards-content {
+    display: flex;
+
+    .cw-dialogcards {
+        flex-grow: 2;
+
+        .scene {
+            margin: 0 auto;
+            width: 440px;
+            height: 600px;
+            perspective: 880px;
+            display: none;
+
+            &.active {
+                display: block;
+                animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
+                transform: translate3d(0, 0, 0);
+                backface-visibility: hidden;
+                perspective: 1000px;
+            }
+        }
+
+        .card {
+            width: 100%;
+            height: 78%;
+            transition: transform 1s;
+            transform-style: preserve-3d;
+            cursor: pointer;
+            position: relative;
+            top: 11%;
+        }
+
+        .card.is-flipped {
+            transform: rotateY(180deg);
+        }
+
+        .card__face {
+            position: absolute;
+            width: 100%;
+            height: 100%;
+            color: var(--black);
+            text-align: center;
+            font-weight: bold;
+            font-size: 1.2em;
+            backface-visibility: hidden;
+            box-shadow: 0 2px 15px fade-out($black, 0.7);
+
+            img {
+                max-width: 380px;
+                max-height: 220px;
+                margin-top: 1em;
+            }
+
+            .cw-dialogcards-front-no-image {
+                @include background-icon(question, navigation, 150);
+            }
+
+            .cw-dialogcards-back-no-image {
+                @include background-icon(exclaim, navigation, 150);
+            }
+
+            .cw-dialogcards-front-no-image,
+            .cw-dialogcards-back-no-image {
+                width: 100%;
+                height: 180px;
+                margin-top: 2em;
+                background-repeat: no-repeat;
+                background-position-x: center;
+            }
+
+            p {
+                margin: 1em 3em 1em 4em;
+                padding-right: 1em;
+                overflow-y: auto;
+                max-height: 12em;
+                text-align: justify;
+            }
+        }
+
+        .card__face--front {
+            @include background-icon(arr_1right, clickable);
+            background-color: var(--white);
+            background-repeat: no-repeat;
+            background-position: 95% 95%;
+        }
+
+        .card__face--back {
+            @include background-icon(arr_1left, clickable);
+            background-color: var(--white);
+            background-repeat: no-repeat;
+            background-position: 5% 95%;
+            transform: rotateY(180deg);
+        }
+    }
+
+    .cw-dialogcards-navbutton {
+        color: transparent;
+        width: 35px;
+        height: 35px;
+        border-radius: 2px;
+        background-position: 50%;
+        background-repeat: no-repeat;
+        background-color: var(--base-color);
+        border: none;
+        display: block;
+        z-index: 4;
+        margin: auto 2px;
+        padding: 0;
+        cursor: pointer;
+
+        &.cw-dialogcards-prev {
+            @include background-icon(arr_1left, info-alt, 24);
+        }
+
+        &.cw-dialogcards-next {
+            @include background-icon(arr_1right, info-alt, 24);
+            right: 0;
+        }
+
+        &.cw-dialogcards-prev-disabled,
+        &.cw-dialogcards-next-disabled {
+            background-color: var(--light-gray-color-40);
+        }
+    }
+
+    @keyframes shake {
+        10%,
+        90% {
+            transform: translate3d(-1px, 0, 0);
+        }
+
+        20%,
+        80% {
+            transform: translate3d(2px, 0, 0);
+        }
+
+        30%,
+        50%,
+        70% {
+            transform: translate3d(-4px, 0, 0);
+        }
+
+        40%,
+        60% {
+            transform: translate3d(4px, 0, 0);
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/document.scss b/resources/assets/stylesheets/scss/courseware/blocks/document.scss
new file mode 100644
index 0000000..fb230b2
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/document.scss
@@ -0,0 +1,176 @@
+.cw-block-document {
+    .cw-pdf-main-container {
+        width: calc(100% - 2px);
+        border: solid thin var(--content-color-40);
+
+        .cw-block-title {
+            border: none;
+            border-bottom: solid thin var(--content-color-40);
+        }
+    }
+    .cw-pdf-toolbar {
+        position: relative;
+        display: flex;
+        flex-direction: row;
+        justify-content: flex-start;
+        align-items: baseline;
+        align-content: space-around;
+        background-color: var(--content-color-20);
+        padding: 4px 8px;
+
+        button {
+            height: 100%;
+            margin: 0 2px 0 0;
+            padding: 4px;
+
+            &.active {
+                background-color: var(--base-color);
+            }
+        }
+
+        .cw-pdf-toolbar-left {
+            position: relative;
+            display: flex;
+            flex-direction: row;
+            justify-content: flex-start;
+            align-items: baseline;
+            align-content: space-between;
+            width: 33%;
+        }
+
+        .cw-pdf-toolbar-middle {
+            position: relative;
+            display: flex;
+            justify-content: center;
+            width: 34%;
+
+            .cw-pdf-zoom-buttons {
+                margin-right: 8px;
+
+                button {
+                    margin: 0;
+                    padding: 4px 0;
+                }
+            }
+        }
+
+        .cw-pdf-toolbar-right {
+            display: flex;
+            flex-direction: row;
+            justify-content: flex-end;
+            align-items: baseline;
+            align-content: space-between;
+            position: relative;
+            width: 33%;
+            margin-right: 4px;
+        }
+
+        .cw-pdf-page-nav {
+            margin: 0 4px;
+
+            button {
+                margin: 0;
+                padding: 4px 0;
+            }
+
+            .cw-pdf-page-num {
+                text-align: right;
+                width: 2em;
+            }
+        }
+
+        .cw-pdf-search-box {
+            position: absolute;
+            top: 33px;
+            left: 22px;
+            width: auto;
+            background-color: var(--content-color-20);
+            border-top: none;
+            padding: 6px;
+            z-index: 2;
+            line-height: normal;
+
+            .cw-pdf-search-num {
+                margin: 4px 0 0 0;
+                display: block;
+            }
+
+            .cw-pdf-search-navs {
+                display: inline-block;
+
+                button {
+                    margin: 0;
+                    padding: 0;
+                }
+            }
+        }
+    }
+
+    .cw-pdf-outer-container {
+        position: relative;
+        width: 100%;
+
+        .cw-pdf-content {
+            display: flex;
+            flex-direction: row;
+
+            .cw-pdf-sidebar {
+                width: 25%;
+                min-width: 270px;
+                align-self: stretch;
+                background-color: var(--white);
+                border-right: solid 1px var(--content-color-40);
+
+                ul.cw-pdf-toc-list,
+                ul.cw-pdf-toc-sub-list {
+                    padding: 0;
+                    list-style: none;
+
+                    li {
+                        padding: 0.5em 1em;
+                    }
+                }
+
+                ul.cw-pdf-toc-list {
+                    margin-top: 1em;
+                }
+            }
+
+            .cw-pdf-viewer-container {
+                width: 100%;
+                height: 100%;
+                overflow: hidden;
+                cursor: text;
+
+                &.hand-cursor-grab {
+                    cursor: grab;
+
+                    &.grabbing {
+                        cursor: grabbing;
+                    }
+                }
+
+                &.has-error {
+                    display: none;
+                }
+
+                .page {
+                    position: relative;
+                    margin: 0 auto;
+                }
+            }
+        }
+
+        .cw-pdf-viewer-fake-container {
+            position: absolute;
+        }
+
+        .cw-pdf-error-page {
+            overflow: hidden;
+            width: calc(100% - 16px);
+            height: 100%;
+            padding: 8px;
+            display: table;
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/embed.scss b/resources/assets/stylesheets/scss/courseware/blocks/embed.scss
new file mode 100644
index 0000000..094afca
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/embed.scss
@@ -0,0 +1,15 @@
+.cw-block-embed {
+    .cw-block-content {
+        .cw-block-embed-iframe-wrapper {
+            overflow-y: hidden;
+
+            iframe {
+                height: 100% !important;
+                width: 100% !important;
+            }
+        }
+        .cw-block-embed-info {
+            margin-top: 0.5em;
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/files.scss b/resources/assets/stylesheets/scss/courseware/blocks/files.scss
new file mode 100644
index 0000000..7bc9727
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/files.scss
@@ -0,0 +1,192 @@
+@use '../../../mixins.scss' as *;
+
+// folder block
+.cw-block-folder-info {
+    border: solid thin var(--content-color-40);
+    padding: 10px 10px 0 10px;
+    overflow: hidden;
+    border-bottom: none;
+}
+
+.cw-block-folder-list {
+    border: solid thin var(--content-color-40);
+    padding: 0;
+    list-style: none;
+
+    .cw-block-folder-file-item {
+        list-style: none;
+
+        &:not(:last-child) {
+            border-bottom: solid thin var(--content-color-40);
+        }
+
+        a {
+            display: block;
+        }
+
+        &:hover {
+            background-color: hsla(217, 6%, 45%, 0.2);
+        }
+    }
+
+    .cw-block-folder-download-icon {
+        @include background-icon(download, clickable, 24);
+        background-repeat: no-repeat;
+        float: right;
+        height: 24px;
+        width: 24px;
+        margin: 1em;
+    }
+}
+
+.cw-block-folder-upload {
+    border: solid thin var(--content-color-40);
+    padding: 1em 10px;
+    border-top: none;
+
+    .cw-file-input {
+        width: calc(100% - 148px);
+        vertical-align: middle;
+    }
+}
+
+// for folder and download block
+.cw-block-file-info {
+    @include background-icon(file, clickable, 24);
+    background-repeat: no-repeat;
+    display: block;
+    padding: 16px 16px 16px 40px;
+    background-position: 10px 16px;
+    width: calc(100% - 56px);
+    overflow: hidden;
+    text-overflow: ellipsis;
+
+    &.cw-block-file-icon-empty {
+        color: var(--black);
+        @include background-icon(folder-empty, info, 24);
+    }
+
+    &.cw-block-file-icon-none {
+        color: var(--black);
+        @include background-icon(file, info, 24);
+    }
+
+    &.cw-block-file-icon-audio {
+        @include background-icon(file-audio, clickable, 24);
+
+        &.download-disabled {
+            @include background-icon(file-audio, info, 24);
+        }
+    }
+
+    &.cw-block-file-icon-pic {
+        @include background-icon(file-pic, clickable, 24);
+
+        &.download-disabled {
+            @include background-icon(file-pic, info, 24);
+        }
+    }
+
+    &.cw-block-file-icon-video {
+        @include background-icon(file-video, clickable, 24);
+
+        &.download-disabled {
+            @include background-icon(file-video, info, 24);
+        }
+    }
+
+    &.cw-block-file-icon-pdf {
+        @include background-icon(file-pdf, clickable, 24);
+
+        &.download-disabled {
+            @include background-icon(file-pdf, info, 24);
+        }
+    }
+
+    &.cw-block-file-icon-word {
+        @include background-icon(file-word, clickable, 24);
+
+        &.download-disabled {
+            @include background-icon(file-word, info, 24);
+        }
+    }
+
+    &.cw-block-file-icon-spreadsheet {
+        @include background-icon(file-excel, clickable, 24);
+
+        &.download-disabled {
+            @include background-icon(file-excel, info, 24);
+        }
+    }
+
+    &.cw-block-file-icon-text {
+        @include background-icon(file-text, clickable, 24);
+
+        &.download-disabled {
+            @include background-icon(file-text, info, 24);
+        }
+    }
+
+    &.cw-block-file-icon-ppt {
+        @include background-icon(file-ppt, clickable, 24);
+
+        &.download-disabled {
+            @include background-icon(file-ppt, info, 24);
+        }
+    }
+
+    &.cw-block-file-icon-archive {
+        @include background-icon(file-archive, clickable, 24);
+
+        &.download-disabled {
+            @include background-icon(file-archive, info, 24);
+        }
+    }
+
+    &.cw-block-file-icon-file {
+        @include background-icon(file, clickable, 24);
+
+        &.download-disabled {
+            @include background-icon(file, info, 24);
+        }
+    }
+}
+.cw-block-file-details {
+    margin-top: -16px;
+    padding-left: 40px;
+    padding-bottom: 16px;
+    color: var(--dark-gray-color);
+}
+.cw-block-file-owner,
+.cw-block-file-mkdate {
+    display: block;
+    width: calc(100% - 56px);
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+// download block
+.cw-block-download {
+    .cw-block-download-content {
+        border: solid thin var(--content-color-40);
+
+        .cw-block-download-file-item {
+            a {
+                display: block;
+            }
+
+            &:hover {
+                background-color: fade-out($dark-gray-color-75, 0.8);
+            }
+
+            .cw-block-download-download-icon {
+                @include background-icon(download, clickable, 24);
+                background-repeat: no-repeat;
+                float: right;
+                height: 24px;
+                width: 24px;
+                margin: 1em;
+            }
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/gallery.scss b/resources/assets/stylesheets/scss/courseware/blocks/gallery.scss
new file mode 100644
index 0000000..9e1cfb4
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/gallery.scss
@@ -0,0 +1,160 @@
+@use '../../../mixins.scss' as *;
+
+.cw-block-gallery {
+    .cw-block-content {
+        overflow: hidden;
+    }
+}
+
+.cw-block-gallery-content {
+    position: relative;
+    margin: auto;
+}
+
+.cw-block-gallery-slides {
+    display: none;
+
+    img {
+        display: block;
+        max-width: 100%;
+        margin-left: auto;
+        margin-right: auto;
+    }
+}
+
+.cw-block-gallery-prev,
+.cw-block-gallery-next {
+    cursor: pointer;
+    position: absolute;
+    background-color: fade-out($white, 0.6);
+    top: 50%;
+    height: 36px;
+    width: 36px;
+    background-repeat: no-repeat;
+    background-position: center;
+    margin-top: -22px;
+    transition: 200ms ease;
+    user-select: none;
+    border: none;
+
+    &:hover {
+        background-color: var(--base-color);
+    }
+}
+
+.cw-block-gallery-prev {
+    @include background-icon(arr_1left, clickable, 24);
+
+    &:hover {
+        @include background-icon(arr_1left, info-alt, 24);
+    }
+}
+
+.cw-block-gallery-next {
+    right: 0;
+    @include background-icon(arr_1right, clickable, 24);
+
+    &:hover {
+        @include background-icon(arr_1right, info-alt, 24);
+    }
+}
+
+.cw-block-gallery-file-description {
+    width: -moz-available;
+    color: var(--white);
+    font-size: 15px;
+    padding: 8px 12px;
+    position: absolute;
+    bottom: 8px;
+    text-align: center;
+
+    p {
+        display: -webkit-inline-box;
+        background-color: fade-out($black, 0.6);
+        padding: 0 2em;
+        margin-bottom: 4px;
+        overflow: hidden;
+        -webkit-line-clamp: 3;
+        -webkit-box-orient: vertical;
+    }
+
+    &.show-on-hover {
+        display: none;
+    }
+}
+
+.cw-block-gallery-content:hover .show-on-hover {
+    display: block;
+}
+
+.cw-block-gallery-number-text {
+    color: var(--white);
+    font-size: 12px;
+    padding: 8px 12px;
+    position: absolute;
+    top: 0;
+    background-color: fade-out($black, 0.6);
+}
+
+.cw-block-gallery-fade {
+    -webkit-animation-name: fade;
+    -webkit-animation-duration: 1.5s;
+    animation-name: fade;
+    animation-duration: 1.5s;
+}
+
+@-webkit-keyframes fade {
+    from {
+        opacity: 0.4;
+    }
+    to {
+        opacity: 1;
+    }
+}
+
+@keyframes fade {
+    from {
+        opacity: 0.4;
+    }
+    to {
+        opacity: 1;
+    }
+}
+
+.cw-block-gallery-grid {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+    list-style: none;
+    padding: 0;
+
+    figure {
+        padding: 1px 4px;
+        margin: unset;
+
+        figcaption {
+            margin-bottom: 12px;
+
+            .cw-block-gallery-grid-file-name {
+                font-weight: 700;
+                margin-bottom: 4px;
+                overflow: hidden;
+                display: -webkit-box;
+                -webkit-line-clamp: 2;
+                -webkit-box-orient: vertical;
+            }
+
+            .cw-block-gallery-grid-file-description {
+                overflow: hidden;
+                display: -webkit-box;
+                -webkit-line-clamp: 5;
+                -webkit-box-orient: vertical;
+            }
+        }
+    }
+}
+
+.cw-container-wrapper-edit .cw-block-gallery-grid {
+    margin: 0;
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/headline.scss b/resources/assets/stylesheets/scss/courseware/blocks/headline.scss
new file mode 100644
index 0000000..3646327
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/headline.scss
@@ -0,0 +1,651 @@
+@use '../../../mixins.scss' as *;
+@import '../gradients.scss';
+@import '../variables.scss';
+
+$big-icon-size: 196;
+$big-icon-size-px: $big-icon-size + px;
+$large-icon-size: 128;
+$large-icon-size-px: $large-icon-size + px;
+
+.cw-block-headline {
+    .cw-block-headline-content {
+        min-height: 600px;
+        overflow: hidden;
+        background-position: center;
+        background-size: $max-content-width;
+        background-repeat: no-repeat;
+
+        &.half {
+            min-height: 300px;
+        }
+
+        &.quarter {
+            min-height: 150px;
+        }
+
+        &.heavy {
+            .cw-block-headline-iconbox {
+                display: none;
+            }
+            .cw-block-headline-textbox {
+                margin: 2em;
+                h1,
+                h2 {
+                    display: inline;
+                    border: none;
+                    padding: 0.25em 0;
+                }
+                h1 {
+                    font-size: 10em;
+                    line-height: 1.2em;
+                }
+                h2 {
+                    font-size: 2em;
+                    line-height: 1em;
+                }
+
+                &.quarter {
+                    margin: 1.5em;
+                    h1 {
+                        font-size: 5em;
+                    }
+                    h2 {
+                        font-size: 1.5em;
+                    }
+                }
+            }
+        }
+        &.bigicon_top {
+            .icon-layer {
+                background-repeat: no-repeat;
+                background-position: center;
+                margin-top: 8em;
+                margin-bottom: 1em;
+                background-size: $big-icon-size-px;
+                height: $big-icon-size-px;
+
+                @each $icon in $icons {
+                    &.icon-black-#{$icon} {
+                        @include background-icon($icon, info, $big-icon-size);
+                    }
+                    &.icon-white-#{$icon} {
+                        @include background-icon($icon, info-alt, $big-icon-size);
+                    }
+                    &.icon-studip-blue-#{$icon} {
+                        @include background-icon($icon, clickable, $big-icon-size);
+                    }
+                    &.icon-studip-red-#{$icon} {
+                        @include background-icon($icon, status-red, $big-icon-size);
+                    }
+                    &.icon-studip-yellow-#{$icon} {
+                        @include background-icon($icon, status-yellow, $big-icon-size);
+                    }
+                    &.icon-studip-green-#{$icon} {
+                        @include background-icon($icon, status-green, $big-icon-size);
+                    }
+                    &.icon-studip-gray-#{$icon} {
+                        @include background-icon($icon, inactive, $big-icon-size);
+                    }
+                }
+
+                &.half {
+                    height: 144px;
+                    background-size: 144px;
+                    background-position: center;
+                    margin-top: 2em;
+                    margin-bottom: 0;
+                }
+                &.quarter {
+                    height: 72px;
+                    background-size: 72px;
+                    background-position: center;
+                    margin-top: 1em;
+                    margin-bottom: 0;
+                }
+            }
+
+            .cw-block-headline-textbox {
+                width: 80%;
+                padding-bottom: 4em;
+                margin: 0 auto;
+                .cw-block-headline-title {
+                    h1 {
+                        border: none;
+                        font-size: 5em;
+                        text-align: center;
+                    }
+                }
+
+                .cw-block-headline-subtitle {
+                    h2 {
+                        border: none;
+                        font-size: 18px;
+                        text-align: center;
+                        margin-top: 10px;
+                    }
+                }
+
+                &.quarter {
+                    padding-bottom: 1em;
+                    .cw-block-headline-title {
+                        h1 {
+                            font-size: 2.25em;
+                            margin: 0;
+                        }
+                    }
+                    .cw-block-headline-subtitle {
+                        h2 {
+                            font-size: 1em;
+                            margin: 0;
+                        }
+                    }
+                }
+            }
+        }
+
+        &.bigicon_before {
+            display: flex;
+            align-items: center;
+            .icon-layer {
+                min-height: $big-icon-size-px;
+                min-width: $big-icon-size-px;
+                margin: 0 28px 0 28px;
+                background-repeat: no-repeat;
+                background-position: left center;
+
+                @each $icon in $icons {
+                    &.icon-black-#{$icon} {
+                        @include background-icon($icon, info, $big-icon-size);
+                    }
+                    &.icon-white-#{$icon} {
+                        @include background-icon($icon, info-alt, $big-icon-size);
+                    }
+                    &.icon-studip-blue-#{$icon} {
+                        @include background-icon($icon, clickable, $big-icon-size);
+                    }
+                    &.icon-studip-red-#{$icon} {
+                        @include background-icon($icon, status-red, $big-icon-size);
+                    }
+                    &.icon-studip-yellow-#{$icon} {
+                        @include background-icon($icon, status-yellow, $big-icon-size);
+                    }
+                    &.icon-studip-green-#{$icon} {
+                        @include background-icon($icon, status-green, $big-icon-size);
+                    }
+                    &.icon-studip-gray-#{$icon} {
+                        @include background-icon($icon, inactive, $big-icon-size);
+                    }
+                }
+
+                &.quarter {
+                    min-height: $large-icon-size-px;
+                    min-width: $large-icon-size-px;
+                    background-size: $large-icon-size-px;
+                }
+            }
+
+            .cw-block-headline-textbox {
+                margin: 2em 1em 2em 0;
+                .cw-block-headline-title {
+                    h1 {
+                        border: none;
+                        font-size: 5em;
+                        text-align: left;
+                    }
+                }
+
+                .cw-block-headline-subtitle {
+                    display: none;
+                }
+            }
+        }
+
+        &.ribbon {
+            display: flex;
+            align-items: center;
+
+            .cw-block-headline-textbox {
+                width: 100%;
+                padding: 1em 0;
+                margin: 3em 0;
+                background-color: fade-out($black, 0.5);
+                .cw-block-headline-title {
+                    h1 {
+                        border: none;
+                        font-size: 5em;
+                        text-align: center;
+                        margin-bottom: 0;
+                    }
+                }
+
+                .cw-block-headline-subtitle {
+                    h2 {
+                        border: none;
+                        font-size: 18px;
+                        text-align: center;
+                        margin-top: 10px;
+                    }
+                }
+            }
+        }
+
+        &.vertical {
+            .cw-block-headline-textbox {
+                .cw-block-headline-title {
+                    position: relative;
+                    top: 0;
+                    left: 5em;
+                    height: 5em;
+                    text-align: center;
+                    width: 600px;
+                    transform-origin: 0 0;
+                    transform: rotate(90deg);
+                    h1 {
+                        text-transform: uppercase;
+                        border: none;
+                        font-size: 2em;
+                        line-height: 2.5em;
+                    }
+                }
+
+                .cw-block-headline-subtitle {
+                    margin: 0 8em;
+                    padding: 2em;
+                    h2 {
+                        margin: 0;
+                        font-weight: 400;
+                    }
+                }
+            }
+            &.half {
+                .cw-block-headline-title {
+                    width: 300px;
+                }
+            }
+            &.quarter {
+                .cw-block-headline-title {
+                    width: 150px;
+                    h1 {
+                        font-size: 1em;
+                        line-height: 5em;
+                    }
+                }
+                .cw-block-headline-subtitle {
+                    margin: -2em 8em 0 8em;
+                    padding: 1em;
+                }
+            }
+        }
+
+        &.skew_text {
+            min-height: 600px;
+
+            .cw-block-headline-textbox {
+                font-size: 7em;
+                text-align: left;
+                text-transform: uppercase;
+                font-weight: 800;
+                font-family: sans-serif;
+                font-style: normal;
+                letter-spacing: -6px;
+                margin: 1.25em 0 1.5em 1em;
+                div {
+                    overflow: hidden;
+                    font-size: 1em;
+                    margin: -1px 0;
+                    white-space: nowrap;
+                    height: 0.8em;
+                    width: calc(100% - 1em);
+
+                    h1,
+                    h2 {
+                        font-size: 0.8em;
+                        margin: 0;
+                        white-space: nowrap;
+                        overflow: hidden;
+                        line-height: 0.8em;
+                        height: 0.8em;
+                    }
+                }
+                .cw-block-headline-title,
+                .cw-block-headline-second-subtitle {
+                    transform: skew(60deg, -15deg) scaley(0.66667);
+                }
+                .cw-block-headline-subtitle {
+                    transform: skew(0deg, -15deg) scaley(1.33333);
+                }
+                .cw-block-headline-subtitle {
+                    position: relative;
+                    left: 0.48em;
+                }
+                .cw-block-headline-second-subtitle {
+                    position: relative;
+                    left: 0.92em;
+                }
+            }
+
+            &.half {
+                min-height: 300px;
+                height: 300px;
+                .cw-block-headline-textbox {
+                    font-size: 4em;
+                    margin: 1.5em 0 1.5em 1em;
+                    div {
+                        width: calc(100% - 9em);
+                    }
+                }
+            }
+            &.quarter {
+                min-height: 150px;
+                height: 150px;
+                .cw-block-headline-textbox {
+                    font-size: 2em;
+                    margin: 0.75em 0 0.75em 1em;
+                    letter-spacing: -2px;
+                    div {
+                        width: calc(100% - 18em);
+                    }
+                }
+            }
+        }
+
+        &.icon_top_lines {
+            .cw-block-headline-iconbox {
+                margin: 32px;
+                &::before,
+                &::after {
+                    width: calc(50% - 96px);
+                    height: 64px;
+                    display: inline-block;
+                    border-top: solid 4px;
+                    content: '';
+                }
+                @each $name, $hex in $border-colors {
+                    &.border-#{'' + $name}::before,
+                    &.border-#{'' + $name}::after {
+                        border-color: $hex;
+                    }
+                }
+                .icon-layer {
+                    background-repeat: no-repeat;
+                    background-position: center;
+                    background-size: 128px;
+                    height: 128px;
+                    width: 192px;
+                    display: inline-block;
+                    margin: auto;
+
+                    @each $icon in $icons {
+                        &.icon-black-#{$icon} {
+                            @include background-icon($icon, info, 128);
+                        }
+                        &.icon-white-#{$icon} {
+                            @include background-icon($icon, info-alt, 128);
+                        }
+                        &.icon-studip-blue-#{$icon} {
+                            @include background-icon($icon, clickable, 128);
+                        }
+                        &.icon-studip-red-#{$icon} {
+                            @include background-icon($icon, status-red, 128);
+                        }
+                        &.icon-studip-yellow-#{$icon} {
+                            @include background-icon($icon, status-yellow, 128);
+                        }
+                        &.icon-studip-green-#{$icon} {
+                            @include background-icon($icon, status-green, 128);
+                        }
+                        &.icon-studip-gray-#{$icon} {
+                            @include background-icon($icon, inactive, 128);
+                        }
+                    }
+
+                    &.half {
+                        height: 128px;
+                        background-size: 128px;
+                    }
+                    &.quarter {
+                        height: 64px;
+                        width: 96px;
+                        background-size: 64px;
+                    }
+                }
+                &.half {
+                    margin: 16px 32px;
+                }
+                &.quarter {
+                    margin: 8px 32px 0 32px;
+                    &::before,
+                    &::after {
+                        width: calc(50% - 48px);
+                        height: 32px;
+                    }
+                }
+            }
+            .cw-block-headline-textbox {
+                width: calc(100% - 64px);
+                min-height: 350px;
+                margin: 0 auto;
+                border-bottom: solid 4px;
+                @each $name, $hex in $border-colors {
+                    &.border-#{'' + $name} {
+                        border-color: $hex;
+                    }
+                }
+                .cw-block-headline-title {
+                    h1 {
+                        border: none;
+                        font-size: 5em;
+                        text-align: center;
+                    }
+                }
+
+                .cw-block-headline-subtitle {
+                    h2 {
+                        border: none;
+                        font-size: 18px;
+                        text-align: center;
+                        margin: 22px 64px;
+                    }
+                }
+
+                &.half {
+                    min-height: 0px;
+                    margin-bottom: 32px;
+                    .cw-block-headline-title {
+                        h1 {
+                            font-size: 4em;
+                        }
+                    }
+
+                    .cw-block-headline-subtitle {
+                        h2 {
+                            font-size: 1em;
+                            margin: 1em 64px;
+                        }
+                    }
+                }
+                &.quarter {
+                    min-height: 0;
+                    margin-bottom: 0;
+                    border: none;
+                    .cw-block-headline-title {
+                        h1 {
+                            font-size: 2em;
+                        }
+                    }
+
+                    .cw-block-headline-subtitle {
+                        h2 {
+                            font-size: 1em;
+                            margin: 8px 64px;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+.cw-container-colspan-half,
+.cw-container-colspan-half-center {
+    .cw-block-headline {
+        .cw-block-headline-content {
+            min-height: 300px;
+
+            &.half {
+                min-height: 150px;
+            }
+            &.heavy {
+                h1 {
+                    font-size: 4.5em;
+                }
+                h2 {
+                    font-size: 1.25em;
+                }
+            }
+            &.bigicon_top {
+                .icon-layer {
+                    background-position: center;
+                    height: 98px;
+                    margin-top: 2em;
+                    margin-bottom: 1em;
+
+                    @each $icon in $icons {
+                        &.icon-black-#{$icon} {
+                            @include background-icon($icon, info, 98);
+                        }
+                        &.icon-white-#{$icon} {
+                            @include background-icon($icon, info-alt, 98);
+                        }
+                        &.icon-studip-blue-#{$icon} {
+                            @include background-icon($icon, clickable, 98);
+                        }
+                        &.icon-studip-red-#{$icon} {
+                            @include background-icon($icon, status-red, 98);
+                        }
+                        &.icon-studip-yellow-#{$icon} {
+                            @include background-icon($icon, status-yellow, 98);
+                        }
+                        &.icon-studip-green-#{$icon} {
+                            @include background-icon($icon, status-green, 98);
+                        }
+                        &.icon-studip-gray-#{$icon} {
+                            @include background-icon($icon, inactive, 98);
+                        }
+                    }
+
+                    &.half {
+                        background-size: 72px;
+                        height: 72px;
+                        background-position: center;
+                    }
+                }
+
+                .cw-block-headline-textbox {
+                    max-width: 80%;
+                    margin: 0 auto;
+                    padding-bottom: 1em;
+                    .cw-block-headline-title {
+                        h1 {
+                            font-size: 2em;
+                        }
+                    }
+
+                    .cw-block-headline-subtitle {
+                        h2 {
+                            font-size: 12px;
+                        }
+                    }
+                }
+            }
+            &.bigicon_before {
+                min-height: 300px;
+                .icon-layer {
+                    min-height: 92px;
+                    min-width: 92px;
+                    background-position: 0 center;
+                    @each $icon in $icons {
+                        &.icon-black-#{$icon} {
+                            @include background-icon($icon, info, 92);
+                        }
+                        &.icon-white-#{$icon} {
+                            @include background-icon($icon, info-alt, 92);
+                        }
+                        &.icon-studip-blue-#{$icon} {
+                            @include background-icon($icon, clickable, 92);
+                        }
+                        &.icon-studip-red-#{$icon} {
+                            @include background-icon($icon, status-red, 92);
+                        }
+                        &.icon-studip-yellow-#{$icon} {
+                            @include background-icon($icon, status-yellow, 92);
+                        }
+                        &.icon-studip-green-#{$icon} {
+                            @include background-icon($icon, status-green, 92);
+                        }
+                        &.icon-studip-gray-#{$icon} {
+                            @include background-icon($icon, inactive, 92);
+                        }
+                    }
+                }
+
+                .cw-block-headline-textbox {
+                    .cw-block-headline-title {
+                        h1 {
+                            font-size: 2.5em;
+                        }
+                    }
+                }
+
+                &.half {
+                    min-height: 150px;
+                }
+            }
+
+            &.ribbon {
+                min-height: 300px;
+
+                &.half {
+                    min-height: 150px;
+                }
+
+                .cw-block-headline-textbox {
+                    margin: 3em 0;
+
+                    .cw-block-headline-title {
+                        h1 {
+                            font-size: 2.5em;
+                        }
+                    }
+                    .cw-block-headline-subtitle {
+                        h2 {
+                            font-size: 12px;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+.responsive-display {
+    .cw-block-headline {
+        .cw-block-headline-content {
+            background-size: 100%;
+            &.bigicon_before {
+                .icon-layer {
+                    background-size: 144px;
+                }
+                .cw-block-headline-textbox .cw-block-headline-title h1 {
+                    font-size: 4em;
+                }
+            }
+            &.skew_text {
+                min-height: 300px;
+                .cw-block-headline-textbox {
+                    font-size: 4em;
+                    letter-spacing: -4px;
+                }
+            }
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/iframe.scss b/resources/assets/stylesheets/scss/courseware/blocks/iframe.scss
new file mode 100644
index 0000000..6bba2ab
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/iframe.scss
@@ -0,0 +1,54 @@
+@use '../../../mixins.scss' as *;
+
+.cw-block-iframe {
+    .cw-block-content {
+        iframe {
+            border: solid thin var(--content-color-40);
+            width: calc(100% - 2px);
+        }
+
+        .cw-block-iframe-cc-data {
+            border: solid thin var(--content-color-40);
+            border-top: none;
+            margin-top: -6px;
+            padding-top: 10px;
+            height: 75px;
+
+            .cw-block-iframe-cc {
+                width: 120px;
+                height: 50px;
+                margin-left: 4px;
+                display: inline-block;
+                background-repeat: no-repeat;
+
+                &.cw-block-iframe-cc-by {
+                    background-image: url('#{$image-path}/cc/by.svg');
+                }
+                &.cw-block-iframe-cc-by-nc {
+                    background-image: url('#{$image-path}/cc/by-nc.eu.svg');
+                }
+                &.cw-block-iframe-cc-by-nc-nd {
+                    background-image: url('#{$image-path}/cc/by-nc-nd.eu.svg');
+                }
+                &.cw-block-iframe-cc-by-nc-sa {
+                    background-image: url('#{$image-path}/cc/by-nc-sa.svg');
+                }
+                &.cw-block-iframe-cc-by-nd {
+                    background-image: url('#{$image-path}/cc/by-nd.svg');
+                }
+                &.cw-block-iframe-cc-by-sa {
+                    background-image: url('#{$image-path}/cc/by-sa.svg');
+                }
+            }
+
+            .cw-block-iframe-cc-infos {
+                display: inline-block;
+                vertical-align: top;
+                padding-left: 1em;
+                p {
+                    margin: 0;
+                }
+            }
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/image-map.scss b/resources/assets/stylesheets/scss/courseware/blocks/image-map.scss
new file mode 100644
index 0000000..398ce99
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/image-map.scss
@@ -0,0 +1,35 @@
+@import '../variables.scss';
+
+.cw-block-image-map {
+    .cw-image-map-canvas,
+    .cw-image-map-original-img {
+        display: none;
+    }
+
+    form.default {
+        label.cw-block-image-map-dimensions > input[type='number'] {
+            display: inline-block;
+        }
+    }
+}
+
+.cw-draggable-shapes-wrapper {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    width: 100%;
+    height: 100%;
+    margin: $cw-wrapper-gap;
+
+    .cw-draggable-area {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        text-align: center;
+        &:hover {
+            cursor: grab;
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/keypoint.scss b/resources/assets/stylesheets/scss/courseware/blocks/keypoint.scss
new file mode 100644
index 0000000..1cf7b86
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/keypoint.scss
@@ -0,0 +1,100 @@
+@use '../../../mixins.scss' as *;
+@import '../variables.scss';
+
+.cw-keypoint-content {
+    display: flex;
+    min-height: 52px;
+    padding: 1.5em 2.5em 1.5em 2.5em;
+    border-style: solid;
+    border-width: 2px;
+    align-items: center;
+}
+
+.cw-keypoint-content > img {
+    flex-shrink: 0;
+}
+
+.cw-keypoint-red {
+    border-color: map-get($icon-colors, 'icon-red');
+}
+
+.cw-keypoint-blue {
+    border-color: map-get($icon-colors, 'icon-blue');
+}
+
+.cw-keypoint-green {
+    border-color: map-get($icon-colors, 'icon-green');
+}
+
+.cw-keypoint-gray {
+    border-color: map-get($icon-colors, 'icon-gray');
+}
+
+.cw-keypoint-yellow {
+    border-color: map-get($icon-colors, 'icon-yellow');
+}
+
+.cw-keypoint-sentence {
+    display: inline;
+    font-size: 1.25em;
+    padding-left: 1.5em;
+    margin-top: 10px;
+}
+
+.cw-keypoint-label-color {
+    width: 32px !important;
+    min-width: 32px !important;
+    height: 32px;
+    padding: 5px !important;
+
+    > .cw-keypoint-input-color {
+        visibility: hidden;
+        position: absolute;
+    }
+
+    > .cw-keypoint-input-color + div {
+        cursor: pointer;
+        border: 2px solid transparent;
+        height: 32px;
+        width: 32px;
+    }
+
+    > .cw-keypoint-input-color:checked + div {
+        /* (RADIO CHECKED) IMAGE STYLES */
+        @include background-icon(accept, info_alt, 24);
+        background-repeat: no-repeat;
+        background-position: center;
+    }
+}
+
+label[for='cw-keypoint-color'] {
+    vertical-align: top;
+}
+
+.cw-keypoint-label-color {
+    display: inline-block !important;
+}
+
+.cw-keypoint-icons {
+    background-color: var(--white);
+}
+
+.cw-keypoint-bg-red {
+    background-color: map-get($icon-colors, 'icon-red');
+}
+
+.cw-keypoint-bg-blue {
+    background-color: map-get($icon-colors, 'icon-blue');
+}
+
+.cw-keypoint-bg-green {
+    background-color: map-get($icon-colors, 'icon-green');
+}
+
+.cw-keypoint-bg-gray {
+    background-color: map-get($icon-colors, 'icon-gray');
+}
+
+.cw-keypoint-bg-yellow {
+    background-color: map-get($icon-colors, 'icon-yellow');
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/link.scss b/resources/assets/stylesheets/scss/courseware/blocks/link.scss
new file mode 100644
index 0000000..eeeab6b
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/link.scss
@@ -0,0 +1,73 @@
+@use '../../../mixins.scss' as *;
+
+.cw-block-link {
+    a {
+        text-decoration: none;
+    }
+
+    .cw-link {
+        border: solid thin var(--content-color-40);
+        color: var(--base-color);
+        height: 20px;
+        padding: 1em;
+
+        .cw-link-title {
+            margin-left: 3em;
+        }
+
+        &:hover {
+            background-color: var(--base-color);
+            border: solid thin var(--base-color);
+            color: var(--white);
+        }
+
+        &.internal {
+            @include background-icon(link-intern, clickable, 28);
+            background-position: 1em 50%;
+            background-repeat: no-repeat;
+
+            &:hover {
+                @include background-icon(link-intern, info-alt, 28);
+            }
+        }
+
+        &.external {
+            @include background-icon(link-extern, clickable, 28);
+            background-position: 1em 50%;
+            background-repeat: no-repeat;
+
+            &:hover {
+                @include background-icon(link-extern, info-alt, 28);
+            }
+
+            .cw-link-og-image {
+                display: inline-block;
+                max-width: 40%;
+                vertical-align: top;
+                margin-right: 2em;
+            }
+
+            .cw-link-og-textwrapper {
+                display: inline-block;
+                max-width: 50%;
+
+                p {
+                    margin: 0;
+                }
+
+                .cw-link-og-site {
+                    font-size: 1.25em;
+                }
+
+                .cw-link-og-title {
+                    font-weight: 600;
+                }
+
+                .cw-link-og-description {
+                    color: var(--base-color-80);
+                    text-align: justify;
+                }
+            }
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/lti.scss b/resources/assets/stylesheets/scss/courseware/blocks/lti.scss
new file mode 100644
index 0000000..c3d7af1
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/lti.scss
@@ -0,0 +1,20 @@
+@use '../../../mixins.scss' as *;
+
+.cw-block-lti {
+    .cw-block-content {
+        .cw-block-lti-content {
+            border: solid thin var(--content-color-40);
+            box-sizing: border-box;
+        }
+
+        .cw-block-lti-icon-tool {
+            @include background-icon(plugin, info, 24);
+            background-repeat: no-repeat;
+            display: block;
+            padding: 16px 16px 16px 40px;
+            background-position: 10px center;
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/table-of-contents.scss b/resources/assets/stylesheets/scss/courseware/blocks/table-of-contents.scss
new file mode 100644
index 0000000..f1c1d22
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/table-of-contents.scss
@@ -0,0 +1,71 @@
+@use '../../../mixins.scss' as *;
+@import '../variables.scss';
+
+.cw-block-table-of-contents {
+    .cw-block-content {
+        overflow: unset;
+    }
+}
+
+.cw-block-table-of-contents-list {
+    padding: 0;
+    list-style: none;
+    border: solid thin var(--content-color-40);
+    li {
+        &:not(:last-child) {
+            border-bottom: solid thin var(--dark-gray-color-30);
+        }
+        a {
+            display: block;
+            padding: 1em;
+        }
+        &:hover {
+            background-color: fade-out($dark-gray-color-75, 0.8);
+        }
+    }
+}
+
+.cw-block-table-of-contents-list-details {
+    padding: 0;
+    list-style: none;
+    border: solid thin var(--content-color-40);
+    li {
+        &:not(:last-child) {
+            border-bottom: solid thin var(--dark-gray-color-30);
+        }
+
+        &:hover {
+            background-color: fade-out($dark-gray-color-75, 0.8);
+        }
+        a {
+            display: block;
+            padding: 1em;
+        }
+    }
+}
+
+.cw-block-table-of-contents-title-box {
+    min-height: 3em;
+    padding-left: 1em;
+    border-left-width: 10px;
+    border-left-style: solid;
+
+    @each $name, $color in $tile-colors {
+        &.#{'' + $name} {
+            border-color: $color;
+        }
+    }
+
+    p,
+    p:hover {
+        color: var(--black);
+    }
+}
+
+.cw-container-colspan-half {
+    .cw-block-table-of-contents-tiles.cw-tiles {
+        .tile {
+            width: 267px;
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/text.scss b/resources/assets/stylesheets/scss/courseware/blocks/text.scss
new file mode 100644
index 0000000..6bb1824
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/text.scss
@@ -0,0 +1,16 @@
+.cw-block-text {
+    .cktoolbar {
+        width: 100% !important;
+        max-width: 100% !important;
+        position: relative !important;
+        top: 0 !important;
+    }
+
+    .cke {
+        width: 100% !important;
+    }
+
+    .ckplaceholder {
+        height: 0 !important;
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/timeline.scss b/resources/assets/stylesheets/scss/courseware/blocks/timeline.scss
new file mode 100644
index 0000000..e727313
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/timeline.scss
@@ -0,0 +1,244 @@
+@import '../variables.scss';
+
+%clearfix {
+    &:after, &:before {
+        content: '';
+        display: block;
+        width: 100%;
+        clear: both;
+    }
+}
+
+.cw-timeline {
+    list-style: none;
+    width: 100%;
+    margin: 30px auto;
+    position: relative;
+    padding: 0;
+    transition: all 0.4s ease;
+
+    &:before {
+        content:"";
+        width: 3px;
+        height: 100%;
+        background: var(--content-color-40);
+        left: 50%;
+        top: 0;
+        position: absolute;
+    }
+
+    &:after {
+        content: "";
+        clear: both;
+        display: table;
+        width: 100%;
+    }
+
+    .cw-timeline-item {
+        margin-bottom: 50px;
+        position: relative;
+        @extend %clearfix;
+
+        .cw-timeline-item-icon {
+            background: var(--white);
+            width: 50px;
+            height: 50px;
+            position: absolute;
+            top: 0;
+            left: 50%;
+            overflow: hidden;
+            margin-left: -25px;
+            @each $name, $color in $tile-colors {
+                &.cw-timeline-item-icon-color-#{"" + $name} {
+                    border: solid 2px $color;
+                }
+            }
+
+            border-radius: 50%;
+
+            img {
+                position: relative;
+                top: 9px;
+                left: 9px;
+
+                &.charcoal {
+                    filter: brightness(0) saturate(100%) invert(22%) sepia(29%) saturate(364%) hue-rotate(168deg) brightness(87%) contrast(79%);
+                }
+                &.royal-purple {
+                    filter: brightness(0) saturate(100%) invert(35%) sepia(43%) saturate(658%) hue-rotate(234deg) brightness(100%) contrast(87%);
+                }
+                &.iguana-green {
+                    filter: brightness(0) saturate(100%) invert(74%) sepia(9%) saturate(1885%) hue-rotate(76deg) brightness(86%) contrast(88%);
+                }
+                &.queen-blue {
+                    filter: brightness(0) saturate(100%) invert(44%) sepia(10%) saturate(2086%) hue-rotate(178deg) brightness(88%) contrast(80%);
+                }
+                &.verdigris {
+                    filter: brightness(0) saturate(100%) invert(64%) sepia(11%) saturate(4959%) hue-rotate(131deg) brightness(103%) contrast(49%);
+                }
+                &.mulberry {
+                    filter: brightness(0) saturate(100%) invert(49%) sepia(16%) saturate(1665%) hue-rotate(271deg) brightness(88%) contrast(95%);
+                }
+                &.pumpkin {
+                    filter: brightness(0) saturate(100%) invert(38%) sepia(86%) saturate(1993%) hue-rotate(13deg) brightness(104%) contrast(108%);
+                }
+                &.sunglow {
+                    filter: brightness(0) saturate(100%) invert(93%) sepia(69%) saturate(6824%) hue-rotate(313deg) brightness(102%) contrast(100%);
+                }
+                &.apple-green {
+                    filter: brightness(0) saturate(100%) invert(69%) sepia(5%) saturate(5203%) hue-rotate(42deg) brightness(100%) contrast(84%);
+                }
+                &.studip-blue {
+                    filter: brightness(0) saturate(100%) invert(26%) sepia(19%) saturate(1783%) hue-rotate(177deg) brightness(96%) contrast(93%);
+                }
+                &.studip-lightblue {
+                    filter: brightness(0) saturate(100%) invert(91%) sepia(12%) saturate(190%) hue-rotate(190deg) brightness(104%) contrast(89%);
+                }
+                &.studip-red {
+                    filter: brightness(0) saturate(100%) invert(8%) sepia(95%) saturate(6904%) hue-rotate(1deg) brightness(95%) contrast(109%);
+                }
+                &.studip-green {
+                    filter: brightness(0) saturate(100%) invert(27%) sepia(85%) saturate(1531%) hue-rotate(109deg) brightness(95%) contrast(101%);
+                }
+                &.studip-yellow {
+                    filter: brightness(0) saturate(100%) invert(94%) sepia(14%) saturate(7314%) hue-rotate(330deg) brightness(103%) contrast(101%);
+                }
+                &.studip-gray {
+                    filter: brightness(0) saturate(100%) invert(46%) sepia(1%) saturate(2621%) hue-rotate(169deg) brightness(87%) contrast(87%);
+                }
+            }
+        }
+
+        .cw-timeline-item-content {
+            width: 40%;
+            background: var(--white);
+            padding: 20px;
+            transition: all var(--transition-duration) ease;
+
+            h3 {
+                padding: 15px;
+                color: var(--white);
+                margin: -20px -20px 0 -20px;
+                font-weight: 700;
+                min-height: 1.1em;
+            }
+
+            article {
+                min-height: 2em;
+                border: solid thin var(--content-color-20);
+                border-top: none;
+                margin: 0 -20px;
+                padding: 15px;
+
+                header {
+                    font-weight: 700;
+                    font-size: 1.1em;
+                    margin: 0.5em 0;
+                }
+            }
+
+            &:before {
+                content: '';
+                position: absolute;
+                left: calc(40% + 40px);
+                top: 18px;
+                width: 0;
+                height: 0;
+                border-top: 7px solid transparent;
+                border-bottom: 7px solid transparent;
+            }
+            &.left {
+                h3 {
+                    text-align: right;
+                }
+            }
+            &.right {
+                float: right;
+
+                h3 {
+                    text-align: left;
+                }
+
+                &:before {
+                    content: '';
+                    right: calc(40% + 40px);
+                    left: inherit;
+                    border-left: 0;
+                }
+            }
+
+            @each $name, $color in $tile-colors {
+                &.cw-timeline-item-content-color-#{"" + $name} {
+                    border-color: $color;
+                    h3 {
+                        background-color: $color;
+                    }
+                    &.left {
+                        &:before {
+                            border-left: 7px solid $color;
+                        }
+                    }
+                    &.right {
+                        &:before {
+                            border-right: 7px solid $color;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+@mixin oneSidedTimeline() {
+    .cw-timeline {
+        &:before {
+            left: 25px;
+        }
+        .cw-timeline-item {
+            .cw-timeline-item-icon {
+                left: 25px;
+            }
+            .cw-timeline-item-content {
+                width: stretch;
+                margin-left: 70px;
+                &.left {
+                    float: unset;
+
+                    h3 {
+                        text-align: left;
+                    }
+
+                    &:before {
+                        content: '';
+                        right: calc(100% - 70px);
+                        left: inherit;
+                        border-left: 0;
+                    }
+                }
+                &.right {
+                    float: unset;
+                    &:before {
+                        right: calc(100% - 70px);
+                    }
+                }
+
+                @each $name, $color in $tile-colors {
+                    &.cw-timeline-item-content-color-#{"" + $name} {
+                        &.left {
+                            &:before {
+                                border-right: 7px solid $color;
+                                border-left: none;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+.cw-container-colspan-half {
+    @include oneSidedTimeline();
+}
+
+@media only screen and (max-width: 1070px) {
+    @include oneSidedTimeline();
+}
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/typewriter.scss b/resources/assets/stylesheets/scss/courseware/blocks/typewriter.scss
new file mode 100644
index 0000000..89a12df
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/typewriter.scss
@@ -0,0 +1,38 @@
+.cw-typewriter-content {
+    .vue-typer {
+        &.font-typewriter {
+            font-family: 'Lucida Sans Typewriter', 'Lucida Console', Monaco, 'Bitstream Vera Sans Mono', monospace;
+        }
+
+        &.font-trebuchet {
+            font-family: 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', sans-serif;
+        }
+
+        &.font-tahoma {
+            font-family: Tahoma, Verdana, Segoe, sans-serif;
+        }
+
+        &.font-georgia {
+            font-family: Georgia, Times, 'Times New Roman', serif;
+        }
+
+        &.font-narrow {
+            font-family: 'Arial Narrow', Arial, 'Helvetica Condensed', Helvetica, sans-serif;
+        }
+
+        &.size-tall {
+            font-size: 1.25em;
+            line-height: 1.25em;
+        }
+
+        &.size-grande {
+            font-size: 1.5em;
+            line-height: 1.25em;
+        }
+
+        &.size-huge {
+            font-size: 2em;
+            line-height: 1.25em;
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/comments.scss b/resources/assets/stylesheets/scss/courseware/comments.scss
new file mode 100644
index 0000000..f953e86
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/comments.scss
@@ -0,0 +1,57 @@
+.cw-structural-element-feedback,
+.cw-structural-element-comments {
+    padding: 0 1em;
+}
+
+.cw-structural-element-feedback-items,
+.cw-structural-element-comments-items,
+.cw-block-feedback-items,
+.cw-block-comments-items {
+    min-height: 1em;
+    max-height: 225px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    margin: -1em -1em 0em 0em;
+    padding: 0em 1em 0em 0em;
+    scroll-behavior: smooth;
+}
+
+.studip-dialog-content {
+    .cw-structural-element-feedback-items,
+    .cw-structural-element-comments-items,
+    .cw-block-feedback-items,
+    .cw-block-comments-items {
+        max-height: unset;
+    }
+}
+
+.cw-structural-element-feedback-create,
+.cw-structural-element-comment-create,
+.cw-block-feedback-create,
+.cw-block-comment-create {
+    border-top: solid thin var(--content-color-40);
+    padding-top: 1em;
+    textarea {
+        width: calc(100% - 6px);
+        resize: none;
+        border: solid thin var(--content-color-40);
+        &:active {
+            border: solid thin var(--content-color-80);
+        }
+    }
+}
+.cw-structural-element-comments-empty,
+.cw-structural-element-feedback-empty,
+.cw-block-comments-empty,
+.cw-block-feedback-empty {
+    .cw-structural-element-feedback-create,
+    .cw-structural-element-comment-create,
+    .cw-block-feedback-create,
+    .cw-block-comment-create {
+        border-top: none;
+    }
+}
+
+.cw-comments-overview-dialog-comments-context {
+    margin: 0 0 1.5em 0;
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/containers/accordion.scss b/resources/assets/stylesheets/scss/courseware/containers/accordion.scss
new file mode 100644
index 0000000..e69de29
diff --git a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss
new file mode 100644
index 0000000..489a1fa
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss
@@ -0,0 +1,202 @@
+.cw-container {
+    margin-bottom: 1em;
+
+    &.cw-container-colspan-full {
+        max-width: $max-content-width;
+        width: 100%;
+    }
+    &.cw-container-colspan-half {
+        max-width: 540px;
+        width: 100%;
+    }
+    &.cw-container-colspan-half-center {
+        width: $max-content-width;
+        .cw-container-content {
+            max-width: 540px;
+            margin: auto;
+        }
+    }
+
+    .cw-container-header {
+        background-color: var(--content-color-20);
+        padding: 4px 10px 4px 22px;
+
+        .cw-container-header-toggle {
+            display: inline-block;
+            width: calc(100% - 40px);
+        }
+
+        span {
+            color: var(--base-color);
+            font-weight: 700;
+            line-height: 2em;
+            font-size: 1.1em;
+
+            &.cw-default-container-blocker-warning {
+                font-weight: 400;
+            }
+        }
+
+        img {
+            vertical-align: text-bottom;
+        }
+
+        .cw-container-actions {
+            position: relative;
+            float: right;
+            margin-top: 4px;
+            // z-index: 31;
+            .is-open {
+                z-index: 31;
+            }
+        }
+    }
+
+    &.cw-container-active {
+        &.cw-container-colspan-half-center {
+            .cw-container-content {
+                max-width: unset;
+                .cw-block-wrapper {
+                    max-width: 540px;
+                    margin: auto;
+                }
+            }
+        }
+        .cw-container-content {
+            border: solid thin var(--content-color-40);
+        }
+    }
+
+
+    .cw-block-wrapper {
+        padding: 0;
+        margin: 0;
+        list-style: none;
+
+        &.cw-block-wrapper-active {
+            padding: 14px 10px;
+
+            .cw-tabs-content {
+                padding: 14px 0;
+            }
+        }
+
+        .cw-block-item {
+            padding: 0;
+            margin: 0 0 1em 0;
+        }
+    }
+
+    .cw-container-list-block-list {
+        padding: 0;
+        list-style: none;
+    }
+
+    .cw-container-tabs-block-list {
+        list-style: none;
+        padding: 1em 1em 0 1em;
+    }
+
+    .cw-container-accordion-block-list {
+        list-style: none;
+        padding: 0 1em;
+    }
+}
+
+.cw-container-section-delete {
+    img {
+        cursor: pointer;
+    }
+}
+
+form.cw-container-dialog-edit-form {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    fieldset {
+        max-width: 200px;
+    }
+}
+
+.cw-container-wrapper-discuss {
+    flex-direction: column;
+
+    .cw-container-colspan-full {
+        max-width: unset;
+    }
+    .cw-container-colspan-half-center,
+    .cw-container-colspan-half {
+        max-width: 1050px;
+    }
+    .cw-container-colspan-half-center {
+        width: 100%;
+        .cw-container-content {
+            width: 1050px;
+        }
+    }
+}
+
+.cw-radioset {
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    margin-bottom: 1em;
+    .cw-radioset-box {
+        width: 128px;
+        height: 128px;
+        text-align: center;
+        margin-right: 16px;
+        border: solid thin var(--content-color-40);
+        &.selected {
+            border-color: var(--base-color);
+            background-color: var(--content-color-20);
+        }
+        &:last-child {
+            margin-right: 0;
+        }
+        label {
+            height: 100%;
+            width: 100%;
+            margin: 0;
+            cursor: pointer;
+            .label-icon {
+                background-position: center 8px;
+                background-repeat: no-repeat;
+                height: 64px;
+                padding: 8px;
+                &.accordion {
+                    @include background-icon(block-accordion, clickable, 64);
+                }
+                &.list {
+                    @include background-icon(view-list, clickable, 64);
+                }
+                &.tabs {
+                    @include background-icon(block-tabs, clickable, 64);
+                }
+                &.full {
+                    @include background-icon(column-full, clickable, 64);
+                }
+                &.half {
+                    @include background-icon(column-half, clickable, 64);
+                }
+                &.half-center {
+                    @include background-icon(column-half-centered, clickable, 64);
+                }
+            }
+
+        }
+        input[type=radio] {
+            position: absolute;
+            opacity: 0;
+            width: 0;
+
+            &:focus + label {
+                outline-color: Highlight;
+                outline-color: -webkit-focus-ring-color;
+                outline-style: auto;
+                outline-width: 1px;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/containers/list.scss b/resources/assets/stylesheets/scss/courseware/containers/list.scss
new file mode 100644
index 0000000..e69de29
diff --git a/resources/assets/stylesheets/scss/courseware/containers/tabs.scss b/resources/assets/stylesheets/scss/courseware/containers/tabs.scss
new file mode 100644
index 0000000..e69de29
diff --git a/resources/assets/stylesheets/scss/courseware/content-courses.scss b/resources/assets/stylesheets/scss/courseware/content-courses.scss
new file mode 100644
index 0000000..16d712c
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/content-courses.scss
@@ -0,0 +1,12 @@
+.cw-content-courses {
+    h2 {
+        margin: 0;
+        font-weight: 400;
+        padding: 5px 0;
+        font-size: 1.4em;
+    }
+
+    ul.cw-tiles {
+        margin-bottom: 20px;
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/dashboard.scss b/resources/assets/stylesheets/scss/courseware/dashboard.scss
new file mode 100644
index 0000000..a3e9dd3
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/dashboard.scss
@@ -0,0 +1,131 @@
+
+.cw-dashboard {
+    display: flex;
+    max-width: 1112px;
+    flex-wrap: wrap;
+
+    .cw-dashboard-box {
+        margin-bottom: 1em;
+        margin-right: 1em;
+
+        &.cw-dashboard-box-full {
+            max-width: $max-content-width;
+            width: calc(100% - 16px);
+        }
+        &.cw-dashboard-box-half {
+            width: calc(50% - 16px);
+        }
+
+        &.cw-collapsible .cw-collapsible-content.cw-collapsible-content-open {
+            padding: 0;
+        }
+    }
+    .cw-dashboard-overview {
+        display: flex;
+        padding: 10px;
+        flex-wrap: wrap;
+        justify-content: center;
+
+    }
+
+    &.cw-dashboard-task-view {
+        display: unset;
+        max-width: unset;
+        flex-wrap: unset;
+
+        .cw-dashboard-tasks-wrapper,
+        .cw-dashboard-students-wrapper {
+            max-height: unset;
+        }
+    }
+}
+
+#course-courseware-dashboard {
+    .action-menu-item a {
+        cursor: pointer;
+    }
+}
+
+.responsive-display {
+    .cw-dashboard {
+        .cw-dashboard-box {
+
+            &.cw-dashboard-box-full {
+                width: 100%
+            }
+            &.cw-dashboard-box-half {
+                width: 100%
+            }
+        }
+    }
+}
+
+.cw-activities-wrapper {
+    max-width: $max-content-width;
+
+    .cw-companion-box {
+        margin: 10px;
+    }
+
+    .cw-activities {
+        list-style: none;
+        padding: 0;
+
+        .cw-activity-item {
+            border-bottom: solid thin var(--content-color-40);
+            padding: 0.5em;
+
+            &:last-child {
+                border: none;
+            }
+
+            p {
+                margin: 0 0 4px 0;
+                img {
+                    padding-right: 0.5em;
+                    vertical-align: text-bottom;
+                }
+            }
+        }
+    }
+}
+
+.cw-dashboard-box {
+    .cw-dashboard-tasks-wrapper,
+    .cw-dashboard-students-wrapper {
+        padding: 10px;
+    }
+}
+
+.cw-dashboard-tasks-wrapper,
+.cw-dashboard-students-wrapper {
+
+    table.default {
+        margin: 0;
+        thead {
+            tr {
+                th {
+                    &.feedback {
+                        min-width: 11em;
+                    }
+                    &.renewal {
+                        min-width: 14em;
+                    }
+                }
+            }
+        }
+        tbody {
+            tr {
+                td {
+                    img {
+                        vertical-align: text-bottom;
+                        &.display-feedback,
+                        &.edit {
+                            cursor: pointer;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/gradients.scss b/resources/assets/stylesheets/scss/courseware/gradients.scss
new file mode 100644
index 0000000..97f8c7d
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/gradients.scss
@@ -0,0 +1,150 @@
+.faded-sun {
+    background: radial-gradient(
+            ellipse farthest-side at 76% 77%,
+            rgba(245, 228, 212, 0.25) 4%,
+            rgba(255, 255, 255, 0) calc(4% + 1px)
+        ),
+        radial-gradient(circle at 76% 40%, #fef6ec 4%, rgba(255, 255, 255, 0) 4.18%),
+        linear-gradient(135deg, #ff0000 0%, #000036 100%),
+        radial-gradient(ellipse at 28% 0%, #ffcfac 0%, rgba(98, 149, 144, 0.5) 100%),
+        linear-gradient(180deg, #cd6e8a 0%, #f5eab0 69%, #d6c8a2 70%, #a2758d 100%);
+    background-blend-mode: normal, normal, screen, overlay, normal;
+}
+
+.romantic-sun {
+    background: linear-gradient(180deg, #ffb7b7 0%, #727272 100%),
+        radial-gradient(60.91% 100% at 50% 0%, #ffd1d1 0%, #260000 100%),
+        linear-gradient(238.72deg, #ffdddd 0%, #720066 100%),
+        linear-gradient(127.43deg, #00ffff 0%, #ff4444 100%),
+        radial-gradient(100.22% 100% at 70.57% 0%, #ff0000 0%, #00ffe0 100%),
+        linear-gradient(127.43deg, #b7d500 0%, #3300ff 100%);
+    background-blend-mode: screen, overlay, hard-light, color-burn, color-dodge, normal;
+}
+
+.bright-rain {
+    background: radial-gradient(50% 123.47% at 50% 50%, #00ff94 0%, #720059 100%),
+        linear-gradient(121.28deg, #669600 0%, #ff0000 100%),
+        linear-gradient(360deg, #0029ff 0%, #8fff00 100%),
+        radial-gradient(100% 164.72% at 100% 100%, #6100ff 0%, #00ff57 100%),
+        radial-gradient(100% 148.07% at 0% 0%, #fff500 0%, #51d500 100%);
+    background-blend-mode: screen, color-dodge, overlay, difference, normal;
+}
+
+.soft-weather {
+    background: radial-gradient(80.99% 100% at 50% 0%, #00ff0a 0%, #36008e 100%),
+        radial-gradient(50% 123.47% at 50% 50%, #efe7c8 0%, #36008e 100%),
+        linear-gradient(301.28deg, #ff006b 0%, #48dd9e 100%),
+        linear-gradient(294.84deg, #5a60e4 0%, #d30000 100%),
+        linear-gradient(52.29deg, #000000 0%, #00ff85 100%),
+        radial-gradient(100% 138.69% at 100% 0%, #0007a5 0%, #ff7a00 100%),
+        radial-gradient(70.41% 100% at 50% 0%, #d5b300 0%, #2200aa 100%);
+    background-blend-mode: screen, screen, lighten, overlay, lighten, difference, normal;
+}
+
+.blue-bar {
+    background: linear-gradient(
+        123deg,
+        #e7ebf1 0%,
+        #e7ebf1 40%,
+        #d4dbe5 calc(40% + 1px),
+        #d4dbe5 60%,
+        #7e92b0 calc(60% + 1px),
+        #7e92b0 75%,
+        #28497c calc(75% + 1px),
+        #28497c 100%
+    );
+}
+.green-bar {
+    background: linear-gradient(
+        123deg,
+        #c5dea0 0%,
+        #c5dea0 40%,
+        #8bbd40 calc(40% + 1px),
+        #8bbd40 60%,
+        #129c94 calc(60% + 1px),
+        #129c94 75%,
+        #66b570 calc(75% + 1px),
+        #66b570 100%
+    );
+}
+
+.paper {
+    background-image: repeating-linear-gradient(0deg, #d8dadc 0px, #d8dadc 1px, transparent 1px, transparent 23px),
+        repeating-linear-gradient(90deg, #d8dadc 0px, #d8dadc 1px, transparent 1px, transparent 23px),
+        linear-gradient(#ffffff, #ffffff);
+}
+.blueprint {
+    background: #28497c;
+    background-image: 
+        radial-gradient(circle at center center, transparent 0%, #28497c 200%),
+        repeating-linear-gradient(0deg, #536d96 0px, #536d96 1px, transparent 1px, transparent 12px),
+        repeating-linear-gradient(90deg, #536d96 0px, #536d96 1px, transparent 1px, transparent 12px),
+        repeating-linear-gradient(0deg, #536d96 0px, #536d96 2px, transparent 1px, transparent 60px),
+        repeating-linear-gradient(90deg, #536d96 0px, #536d96 2px, transparent 1px, transparent 60px),
+        linear-gradient(#1f3f70, #1f3f70);
+}
+
+.color-blue {
+    background: #28497c;
+    background: -webkit-linear-gradient(90deg, #28497c, #28497c, #7e92b0);
+    background: linear-gradient(90deg, #28497c, #28497c, #7e92b0);
+}
+.color-purple {
+    background: #682c8b;
+    background: -webkit-linear-gradient(90deg, #682c8b, #682c8b, #a480b9);
+    background: linear-gradient(90deg, #682c8b, #682c8b, #a480b9);
+}
+.color-violet {
+    background: #af2d7b;
+    background: -webkit-linear-gradient(90deg, #af2d7b, #af2d7b, #cf81b0);
+    background: linear-gradient(90deg, #af2d7b, #af2d7b, #cf81b0);
+}
+.color-red {
+    background: #d60000;
+    background: -webkit-linear-gradient(90deg, #d60000, #d60000, #e76666);
+    background: linear-gradient(90deg, #d60000, #d60000, #e76666);
+}
+.color-orange {
+    background: #f26e00;
+    background: -webkit-linear-gradient(90deg, #f26e00, #f26e00, #f7a866);
+    background: linear-gradient(90deg, #f26e00, #f26e00, #f7a866);
+}
+.color-yellow {
+    background: #ffbd33;
+    background: -webkit-linear-gradient(90deg, #ffbd33, #ffbd33, #ffd785);
+    background: linear-gradient(90deg, #ffbd33, #ffbd33, #ffd785);
+}
+.color-green {
+    background: #6ead10;
+    background: -webkit-linear-gradient(90deg, #6ead10, #6ead10, #a8ce70);
+    background: linear-gradient(90deg, #6ead10, #6ead10, #a8ce70);
+}
+.color-petrol {
+    background: #129c94;
+    background: -webkit-linear-gradient(90deg, #129c94, #129c94, #70c3bf);
+    background: linear-gradient(90deg, #129c94, #129c94, #70c3bf);
+}
+.color-grey {
+    background: #3c454e;
+    background: -webkit-linear-gradient(90deg, #3c454e, #3c454e, #898f94);
+    background: linear-gradient(90deg, #3c454e, #3c454e, #898f94);
+}
+
+.grid {
+    background-image: radial-gradient(circle at center center, transparent 0%, #000000 95%),
+        repeating-linear-gradient(
+            45deg,
+            #3c454e 0,
+            #3c454e 1px,
+            transparent 1px,
+            transparent 7px
+        ),
+        repeating-linear-gradient(
+            135deg,
+            #3c454e 0,
+            #3c454e 1px,
+            transparent 1px,
+            transparent 14px
+        ),
+        linear-gradient(135deg, #000000, #000000);
+}
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/collapsible.scss b/resources/assets/stylesheets/scss/courseware/layouts/collapsible.scss
new file mode 100644
index 0000000..79dc2ee
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/layouts/collapsible.scss
@@ -0,0 +1,46 @@
+@use '../../../mixins.scss' as *;
+
+.cw-collapsible {
+    border: solid thin var(--content-color-40);
+    margin-bottom: -1px;
+
+    .cw-collapsible-title {
+        @include background-icon(arr_1right, clickable);
+        background-position: 6px center;
+        background-repeat: no-repeat;
+        background-color: var(--content-color-20);
+        padding: 0.5em 2em;
+        margin: 0;
+        font-weight: 600;
+        color: var(--base-color);
+        cursor: pointer;
+
+        &.cw-collapsible-open {
+            @include background-icon(arr_1down, clickable);
+
+            .cw-container-list-sort-mode {
+                margin-top: 8px;
+            }
+        }
+
+        img {
+            vertical-align: top;
+        }
+    }
+
+    .cw-collapsible-content {
+        display: none;
+        &.cw-collapsible-content-open {
+            display: block;
+            padding: 10px;
+        }
+    }
+}
+
+form .cw-collapsible .cw-collapsible-content.cw-collapsible-content-open {
+    padding: unset;
+
+    label {
+        margin: 1.5ex;
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/companion.scss b/resources/assets/stylesheets/scss/courseware/layouts/companion.scss
new file mode 100644
index 0000000..cf48c8f
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/layouts/companion.scss
@@ -0,0 +1,90 @@
+@use '../../../mixins.scss' as *;
+
+$companion-types: (
+    default: basic,
+    unsure: unsure,
+    special: special,
+    alert: alert,
+    sad: sad,
+    happy: happy,
+    pointing: pointing-right,
+    curious: curious
+);
+
+.cw-companion-overlay {
+    position: fixed;
+    bottom: 46px;
+    right: 0;
+    width: 360px;
+    max-width: calc(100% - 140px);
+    height: 120px;
+    z-index: 42000;
+    border: solid thin var(--content-color-40);
+    background-color: var(--white);
+    background-repeat: no-repeat;
+    background-position: 1em center;
+    background-size: 100px;
+    box-shadow: 5px 5px var(--dark-gray-color-10);
+    padding-left: 120px;
+    transform: translateX(100%);
+    transition: transform .5s ease;
+
+    @each $type, $image in $companion-types {
+        &.#{$type} {
+            background-image: url("#{$image-path}/companion/Tin_#{$image}.svg");
+        }
+    }
+
+    &.cw-companion-overlay-in {
+        transform: translateX(0);
+        right: 12px;
+    }
+
+    .cw-companion-overlay-content {
+        display: inline-block;
+        position: relative;
+        top: 25%;
+        padding: 0 1em;
+    }
+
+    .cw-compantion-overlay-close {
+        @include background-icon(decline);
+        background-color: var(--white);
+        background-repeat: no-repeat;
+
+        position: absolute;
+        top: 7px;
+        right: 7px;
+        height: 16px;
+        width: 16px;
+        border: none;
+        cursor: pointer;
+    }
+}
+
+.cw-companion-box {
+    display: flex;
+    height: 120px;
+    border: solid thin var(--content-color-40);
+    background-color: var(--white);
+    background-repeat: no-repeat;
+    background-position: 1em center;
+    background-size: 100px;
+    padding-left: 120px;
+    align-items: center;
+    margin-bottom: 1em;
+
+    @each $type, $image in $companion-types {
+        &.#{$type} {
+            background-image: url("#{$image-path}/companion/Tin_#{$image}.svg");
+        }
+    }
+
+    &.cw-companion-box-in-form {
+        margin-top: 8px;
+    }
+
+    p {
+        margin: 0 1em 10px 0;
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/import-zip.scss b/resources/assets/stylesheets/scss/courseware/layouts/import-zip.scss
new file mode 100644
index 0000000..5ffd9a2
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/layouts/import-zip.scss
@@ -0,0 +1,22 @@
+.cw-import-zip {
+    margin-bottom: 1em;
+
+    header {
+        font-size: 1.15;
+        font-weight: 700;
+    }
+    .progress-bar-wrapper {
+        width: 100%;
+        border: solid thin var(--content-color-40);
+
+        .progress-bar {
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            color: white;
+            text-align: center;
+            white-space: nowrap;
+            background-color: var(--base-color);
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/input-file.scss b/resources/assets/stylesheets/scss/courseware/layouts/input-file.scss
new file mode 100644
index 0000000..9159069
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/layouts/input-file.scss
@@ -0,0 +1,36 @@
+.cw-file-input {
+    width: stretch;
+    border: solid thin var(--base-color);
+    font-size: 14px;
+    cursor: pointer;
+
+    &::file-selector-button {
+        font-family: inherit;
+        border: none;
+        border-right: solid thin var(--base-color);
+        background-color: var(--white);
+        padding: 6px 15px;
+        margin-right: 10px;
+        color: var(--base-color);
+
+        &:hover {
+            background-color: var(--base-color);
+            color: var(--white);
+        }
+    }
+}
+.cw-file-input-change {
+    border: solid thin var(--base-color);
+
+    button.button {
+        padding: 0.5em 1.5em;
+        margin: 0 0 0 -1px;
+        line-height: 100%;
+        border: none;
+        border-right: solid thin var(--base-color);
+    }
+
+    span {
+        padding: 0.5em 1.5em 0.5em 0.5em;
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/progress.scss b/resources/assets/stylesheets/scss/courseware/layouts/progress.scss
new file mode 100644
index 0000000..822294f
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/layouts/progress.scss
@@ -0,0 +1,160 @@
+.cw-unit-progress {
+    .cw-unit-progress-breadcrumb {
+        padding: 10px 0;
+
+        a img {
+            vertical-align: top;
+        }
+    }
+
+    .cw-unit-progress-chapter {
+        text-align: center;
+        margin-bottom: -3.5em;
+
+        h1 {
+            border: none;
+            margin: 0;
+            padding: 0;
+        }
+
+        .cw-progress-circle {
+            font-size: 18px;
+            margin: 1em auto;
+
+            &.cw-unit-progress-current {
+                font-size: 12px;
+                top: -4.5em;
+                left: -2.5em;
+            }
+        }
+    }
+
+    .cw-unit-progress-subchapter-list {
+        border-top: solid thin var(--content-color-40);
+        padding: 0 1em 0 1em;
+
+        .cw-unit-empty-info {
+            margin-top: 10px;
+        }
+    }
+}
+
+.cw-unit-progress-item {
+    display: block;
+    border-bottom: solid thin var(--content-color-40);
+    padding: 10px 0;
+
+    &:hover {
+        background-color: hsla(217, 6%, 45%, 0.2);
+    }
+
+    &:last-child {
+        border: none;
+    }
+
+    .cw-unit-progress-item-value,
+    .cw-unit-progress-item-description {
+        display: inline-block;
+        vertical-align: top;
+    }
+
+    .cw-unit-progress-item-value {
+        width: 70px;
+        color: var(--base-color);
+        font-size: xx-large;
+
+        .cw-progress-circle {
+            font-size: 12px;
+            margin: 4px;
+        }
+    }
+
+    .cw-unit-progress-item-description {
+        color: var(--base-color);
+        padding: 0.5em 0 0 1em;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+    }
+}
+
+.cw-progress-circle {
+    font-size: 14px;
+    margin: 10px;
+    position: relative;
+    padding: 0;
+    width: 5em;
+    height: 5em;
+    background-color: var(--dark-gray-color-10);
+    border-radius: 50%;
+    line-height: 5em;
+
+    &:after {
+        border: none;
+        position: absolute;
+        top: 0.35em;
+        left: 0.35em;
+        text-align: center;
+        display: block;
+        border-radius: 50%;
+        width: 4.3em;
+        height: 4.3em;
+        background-color: white;
+        content: ' ';
+    }
+
+    span {
+        position: absolute;
+        line-height: 5em;
+        width: 5em;
+        text-align: center;
+        display: block;
+        color: var(--base-color);
+        z-index: 2;
+    }
+
+    .left-half-clipper {
+        border-radius: 50%;
+        width: 5em;
+        height: 5em;
+        position: absolute;
+        clip: rect(0, 5em, 5em, 2.5em);
+    }
+
+    &.over50 .left-half-clipper {
+        clip: rect(auto, auto, auto, auto);
+    }
+
+    .value-bar {
+        position: absolute;
+        clip: rect(0, 2.5em, 5em, 0);
+        width: 5em;
+        height: 5em;
+        border-radius: 50%;
+        border: 0.45em solid var(--base-color);
+        box-sizing: border-box;
+    }
+
+    &.over50 .first50-bar {
+        position: absolute;
+        clip: rect(0, 5em, 5em, 2.5em);
+        background-color: var(--base-color);
+        border-radius: 50%;
+        width: 5em;
+        height: 5em;
+    }
+
+    &:not(.over50) .first50-bar {
+        display: none;
+    }
+
+    &.p0 .value-bar {
+        display: none;
+    }
+
+    @for $i from 1 through 100 {
+        &.p#{$i} .value-bar {
+            transform: rotate(calc(360deg * $i / 100));
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss b/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss
new file mode 100644
index 0000000..b39687f
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss
@@ -0,0 +1,359 @@
+@use '../../../mixins.scss' as *;
+
+$consum_ribbon_width: calc(100% - 58px);
+#course-courseware-courseware,
+#contents-courseware-courseware,
+#contents-courseware-shared_content_courseware {
+    &.consume {
+        overflow: hidden;
+    }
+
+    #content-wrapper {
+        position: relative;
+    }
+}
+
+.cw-ribbon-wrapper-consume {
+    position: fixed;
+    padding: 15px;
+    background-color: var(--white);
+    width: $consum_ribbon_width;
+    height: 46px;
+    z-index: 42;
+}
+
+.cw-ribbon-consume-bottom {
+    position: fixed;
+    top: 75px;
+    height: 15px;
+    left: 0;
+    width: calc(100% - 1em);
+    background-color: var(--white);
+    z-index: 40;
+}
+
+.cw-ribbon-sticky-top {
+    position: fixed;
+    top: 40px;
+    height: 20px;
+    width: calc(100% - 314px);
+    background-color: var(--white);
+    z-index: 39;
+}
+
+.cw-ribbon-sticky-bottom {
+    position: fixed;
+    top: 110px;
+    height: 16px;
+    width: calc(100% - 314px);
+    background-color: var(--white);
+    z-index: 39;
+}
+
+.cw-ribbon-sticky-spacer {
+    height: 80px;
+}
+
+.cw-ribbon {
+    display: flex;
+    flex-wrap: wrap;
+    height: auto;
+    min-height: 30px;
+    border: solid thin var(--dark-gray-color-30);
+    margin-bottom: 15px;
+    padding: 1em;
+    justify-content: space-between;
+    background-color: var(--dark-gray-color-5);
+
+    &.cw-ribbon-sticky {
+        position: fixed;
+        top: 50px;
+        width: calc(100% - 346px);
+        z-index: 40;
+    }
+
+    &.cw-ribbon-consume {
+        width: $consum_ribbon_width;
+        position: fixed;
+        margin-bottom: 0;
+    }
+
+    .cw-ribbon-wrapper-left {
+        display: flex;
+        max-width: calc(100% - 106px);
+
+        .cw-ribbon-nav {
+            display: flex;
+            min-width: 75px;
+
+            &.single-icon {
+                min-width: 45px;
+            }
+        }
+
+        .cw-ribbon-breadcrumb {
+            font-size: 1.25em;
+            line-height: 1.5em;
+            margin-right: 1em;
+            min-width: 0;
+
+            ul {
+                display: flex;
+                list-style: none;
+                padding-left: 0;
+
+                li + li:before {
+                    padding: 0 0.25em;
+                    content: '/';
+                    background-repeat: no-repeat;
+                    background-position: center;
+                }
+
+                .cw-ribbon-breadcrumb-item {
+                    display: inline;
+                    flex-shrink: 100000;
+                    min-width: 0;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
+                    white-space: nowrap;
+
+                    a {
+                        color: var(--base-color);
+                        text-decoration: none;
+                        &:hover {
+                            color: var(--active-color);
+                        }
+                    }
+
+                    img {
+                        vertical-align: text-top;
+                    }
+
+                    &.cw-ribbon-breadcrumb-item-current {
+                        flex-shrink: 1;
+                    }
+                }
+            }
+        }
+    }
+
+    .cw-ribbon-wrapper-right {
+        display: flex;
+
+        button {
+            border: none;
+            background-color: transparent;
+            cursor: pointer;
+        }
+    }
+
+    .cw-ribbon-button {
+        height: 24px;
+        width: 24px;
+        margin: 0 7px;
+        padding: 1px 2px;
+        border: none;
+        background-color: transparent;
+        background-repeat: no-repeat;
+        background-position: center;
+        background-size: 24px;
+        cursor: pointer;
+
+        &.cw-ribbon-button-menu {
+            @include background-icon(table-of-contents, clickable, 24);
+        }
+
+        &.cw-ribbon-button-prev {
+            @include background-icon(arr_1left, clickable, 24);
+            margin: 0 0.5em 0 0;
+        }
+
+        &.cw-ribbon-button-next {
+            @include background-icon(arr_1right, clickable, 24);
+            margin: 0 1em 0 0;
+        }
+
+        &.cw-ribbon-button-prev-disabled {
+            @include background-icon(arr_1left, inactive, 24);
+            margin: 0 0.5em 0 0;
+            cursor: default;
+        }
+
+        &.cw-ribbon-button-next-disabled {
+            @include background-icon(arr_1right, inactive, 24);
+            margin: 0 1em 0 0;
+            cursor: default;
+        }
+    }
+
+    .cw-ribbon-action-menu {
+        vertical-align: text-top;
+        margin: 2px 0 0 2px;
+        &.is-open {
+            z-index: 32;
+        }
+    }
+}
+
+.cw-ribbon-tools {
+    background-color: var(--white);
+    border: solid thin var(--content-color-40);
+    box-shadow: 2px 2px var(--dark-gray-color-30);
+    position: absolute;
+    right: -570px;
+    top: 15px;
+    height: 100%;
+    max-width: calc(100% - 28px);
+    display: flex;
+    flex-flow: row;
+    transition: right 0.8s;
+    z-index: 42;
+
+    &.unfold {
+        right: 0px;
+        margin-right: 15px;
+    }
+
+    &.cw-ribbon-tools-consume {
+        position: fixed;
+
+        &.unfold {
+            right: 15px;
+        }
+    }
+
+    &.cw-ribbon-tools-sticky {
+        position: absolute;
+        top: -1px;
+        margin-right: 0;
+    }
+
+    .cw-ribbon-tool-content {
+        height: 100%;
+        width: 540px;
+        background-color: var(--white);
+        padding: 0;
+        overflow: hidden;
+
+        .cw-ribbon-tool-content-nav {
+            position: sticky;
+            height: 100%;
+            top: 0;
+            background-color: var(--white);
+            margin: 0;
+            padding: 0;
+            color: var(--base-color);
+            display: flex;
+            z-index: 43;
+
+            .cw-tools-hide-button {
+                position: absolute;
+                border: none;
+                height: 36px;
+                width: 24px;
+                min-width: 24px;
+                margin-right: 1em;
+                padding: 0 4px;
+                right: 0;
+                top: 12px;
+                cursor: pointer;
+                @include background-icon(decline, clickable, 24);
+                background-repeat: no-repeat;
+                background-size: 24px;
+                background-position: center right;
+                background-color: var(--white);
+            }
+
+            > .cw-ribbon-tool-content-tablist {
+                width: 100%;
+                height: 100%;
+
+                > .cw-tabs-nav {
+                    border: none;
+                    width: calc(100% - 48px);
+
+                    > button {
+                        padding: 18px 8px 4px 8px;
+                        margin-top: 2px;
+                        max-width: unset;
+                        flex-grow: 0.5;
+                        &::after {
+                            margin-top: 16px;
+                        }
+                    }
+                }
+
+                > .cw-tabs-content {
+                    border: none;
+                    border-top: solid thin var(--content-color-40);
+                    padding: 0;
+                    height: calc(100% - 58px);
+
+                    > .cw-tab {
+                        height: calc(100% - 22px);
+                        padding: 14px 8px 8px 8px;
+                        position: relative;
+                        overflow-y: auto;
+                        overflow-x: hidden;
+                        scrollbar-width: thin;
+                        scrollbar-color: var(--base-color) var(--white);
+
+                        &.cw-ribbon-tool-blockadder-tab {
+                            height: 100%;
+                            overflow: hidden;
+                            padding: 0;
+                        }
+                    }
+                }
+            }
+        }
+
+        .cw-ribbon-tool {
+            padding: 14px 8px 6px 8px;
+            height: calc(100% - 64px);
+            overflow-y: auto;
+            overflow-x: hidden;
+            scrollbar-width: thin;
+            scrollbar-color: var(--base-color) var(--white);
+        }
+    }
+}
+
+#courseware-public-index {
+    .cw-ribbon-tools {
+        top: 127px;
+
+        &.cw-ribbon-tools-consume {
+            top: 14px;
+        }
+
+        &.cw-ribbon-tools-sticky {
+            top: 56px;
+        }
+    }
+}
+
+.cw-structural-element-consumemode {
+    .cw-ribbon-tools {
+        top: 25px;
+    }
+}
+
+.responsive-display {
+    .cw-ribbon-sticky-top,
+    .cw-ribbon-sticky-bottom,
+    .cw-ribbon-wrapper-consume,
+    .cw-ribbon-consume-bottom {
+        width: 100%;
+    }
+
+    .cw-ribbon {
+        &.cw-ribbon-sticky {
+            width: calc(100% - 62px);
+        }
+    }
+
+    .cw-ribbon-sticky-spacer {
+        height: 75px;
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/tabs.scss b/resources/assets/stylesheets/scss/courseware/layouts/tabs.scss
new file mode 100644
index 0000000..aa12b5e
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/layouts/tabs.scss
@@ -0,0 +1,114 @@
+@use '../../../mixins.scss' as *;
+@import '../variables.scss';
+
+.cw-tabs-nav {
+    display: flex;
+    flex-wrap: wrap;
+    list-style: none;
+    padding: 0;
+    margin: 0;
+    border: solid thin var(--content-color-40);
+    border-bottom: none;
+
+    button {
+        background-color: var(--white);
+        border: none;
+        padding: 1em 0 4px 0;
+        margin: 0 7px 0 21px;
+        color: var(--base-color);
+        cursor: pointer;
+        text-align: center;
+        flex-grow: 1;
+        max-width: max-content;
+
+        &::after {
+            display: block;
+            margin-top: 4px;
+            margin-bottom: -5px;
+            margin-left: -14px;
+            width: calc(100% + 28px);
+            content: '';
+            border-bottom: solid 3px var(--dark-gray-color-75);
+            transform: scaleX(0);
+            transition: transform var(--transition-duration) ease-in-out;
+        }
+
+        &.is-active,
+        &:hover {
+            color: var(--black);
+            &:after {
+                transform: scaleX(1);
+            }
+        }
+
+        @each $icon in $icons {
+            &.cw-tabs-nav-icon-text-#{$icon} {
+                &::before {
+                    @include background-icon($icon);
+                    background-repeat: no-repeat;
+                    background-position: left bottom;
+
+                    display: inline-block;
+                    height: 16px;
+                    width: 16px;
+                    margin-bottom: -2px;
+                    padding-left: 4px;
+                    content: '';
+                }
+            }
+            &.is-active.cw-tabs-nav-icon-text-#{$icon},
+            &.cw-tabs-nav-icon-text-#{$icon}:hover {
+                &::before {
+                    @include background-icon($icon, info);
+                }
+            }
+        }
+        @each $icon in $icons {
+            &.cw-tabs-nav-icon-solo-#{$icon} {
+                &::before {
+                    display: inline-block;
+                    height: 24px;
+                    width: 24px;
+                    content: '';
+
+                    @include background-icon($icon, clickable, 24);
+                    background-repeat: no-repeat;
+                    background-position: center;
+                }
+            }
+            &.is-active.cw-tabs-nav-icon-solo-#{$icon},
+            &.cw-tabs-nav-icon-solo-#{$icon}:hover {
+                &::before {
+                    @include background-icon($icon, info);
+                }
+            }
+        }
+    }
+
+    &:hover li {
+        &.is-active::after {
+            transform: scaleX(0);
+        }
+        &:hover::after {
+            transform: scaleX(1);
+        }
+    }
+}
+
+.cw-tabs-content {
+    border: solid thin var(--content-color-40);
+    padding: 4px;
+}
+
+.cw-tabs {
+    .cw-tab {
+        display: none;
+        height: 0;
+
+        &.cw-tab-active {
+            display: block;
+            height: unset;
+            padding: 4px 8px;
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/talk-bubble.scss b/resources/assets/stylesheets/scss/courseware/layouts/talk-bubble.scss
new file mode 100644
index 0000000..06062b6
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/layouts/talk-bubble.scss
@@ -0,0 +1,58 @@
+.cw-talk-bubble {
+    margin: 10px 20px;
+    position: relative;
+    width: 85%;
+    height: auto;
+    background-color: var(--dark-gray-color-10);
+    float: left;
+
+    .cw-talk-bubble-talktext {
+        padding: 1em;
+        text-align: left;
+        line-height: 1.5em;
+
+        .cw-talk-bubble-talktext-time {
+            color: var(--dark-gray-color-80);
+            text-align: right;
+            font-size: 0.8em;
+            margin-bottom: -0.5em;
+        }
+    }
+
+    &.cw-talk-bubble-own-post {
+        float: right;
+    }
+
+    &:after {
+        content: ' ';
+        position: absolute;
+        width: 0;
+        height: 0;
+        top: 0px;
+        bottom: auto;
+        border: 22px solid;
+        border-color: var(--dark-gray-color-10) transparent transparent transparent;
+        left: -20px;
+        right: auto;
+    }
+
+    &.cw-talk-bubble-own-post:after {
+        left: auto;
+        right: -20px;
+    }
+
+    .cw-talk-bubble-user {
+        padding: 1em 1em 0 1em;
+
+        .cw-talk-bubble-avatar {
+            display: inline-block;
+        }
+
+        span {
+            padding-left: 4px;
+            color: var(--dark-gray-color-45);
+            font-weight: 600;
+            vertical-align: top;
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/tile.scss b/resources/assets/stylesheets/scss/courseware/layouts/tile.scss
new file mode 100644
index 0000000..f4c795f
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/layouts/tile.scss
@@ -0,0 +1,157 @@
+.cw-tiles {
+    list-style: none;
+    display: flex;
+    flex-wrap: wrap;
+    padding-left: 0;
+    row-gap: 5px;
+    column-gap: 5px;
+}
+.cw-tiles .tile,
+.cw-tile {
+    height: 420px;
+    width: 270px;
+    margin: 0;
+    background-color: var(--base-color);
+    &:last-child {
+        margin-right: 0;
+    }
+
+    @each $name, $color in $tile-colors {
+        &.#{'' + $name} {
+            background-color: $color;
+        }
+    }
+
+    .preview-image {
+        height: 180px;
+        width: 100%;
+        background-size: 100% auto;
+        background-repeat: no-repeat;
+        background-color: var(--content-color-20);
+        background-position: center;
+        &.default-image {
+            @include background-icon(courseware, clickable, 128);
+        }
+
+        .overlay-text {
+            padding: 6px 7px;
+            margin: 4px;
+            background-color: rgba(255, 255, 255, 0.8);
+            width: fit-content;
+            max-width: 100%;
+            height: 1.25em;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            float: right;
+            text-align: right;
+        }
+
+        .overlay-action-menu {
+            padding: 0;
+            margin: 0.25em;
+            background-color: rgba(255, 255, 255, 0.8);
+            width: fit-content;
+            max-width: 100%;
+            overflow: hidden;
+            float: right;
+            text-align: right;
+            .action-menu {
+                margin: 5px;
+            }
+        }
+    }
+
+    .description {
+        height: 220px;
+        padding: 14px;
+        color: var(--white);
+        position: relative;
+        display: block;
+
+        header {
+            font-size: 20px;
+            line-height: 22px;
+            color: var(--white);
+            border: none;
+            width: 240px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            background-repeat: no-repeat;
+            background-position: 0 0;
+
+            @each $type, $icon in $element-icons {
+                &.description-icon-#{$type} {
+                    width: 212px;
+                    padding-left: 28px;
+                    @include background-icon(#{$icon}, info_alt, 22);
+                }
+            }
+        }
+
+        .progress-wrapper {
+            width: 100%;
+            padding: 1em 0;
+            border: none;
+            background: none;
+
+            progress {
+                appearance: none;
+                display: block;
+                width: 100%;
+                height: 3px;
+                margin: 0;
+                border: none;
+                background: rgba(0, 0, 0, 0.3);
+                &::-webkit-progress-bar {
+                    background: rgba(0, 0, 0, 0.3);
+                }
+                &::-webkit-progress-value {
+                    background: white;
+                }
+                &::-moz-progress-bar {
+                    background: white;
+                }
+            }
+        }
+
+        .description-text-wrapper {
+            overflow: hidden;
+            height: 10em;
+            margin-top: 0.5em;
+            display: -webkit-box;
+            margin-bottom: 1em;
+            -webkit-line-clamp: 7;
+            -webkit-box-orient: vertical;
+            p {
+                text-align: left;
+            }
+        }
+
+        footer {
+            width: 242px;
+            text-align: right;
+            color: var(--white);
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+
+            img {
+                vertical-align: text-bottom;
+            }
+        }
+    }
+
+    a[href].description {
+        transition: unset;
+    }
+
+    a.description,
+    a.description:link,
+    a.description:visited,
+    a.description:hover {
+        height: 210px;
+        color: var(--white);
+        text-decoration: unset;
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/tree.scss b/resources/assets/stylesheets/scss/courseware/layouts/tree.scss
new file mode 100644
index 0000000..5f976a2
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/layouts/tree.scss
@@ -0,0 +1,163 @@
+.cw-tree {
+    ol {
+        list-style: none;
+        padding-left: 1.25em;
+        margin-bottom: 20px;
+
+        &.cw-tree-root-list {
+            padding-left: 0;
+
+            > li.cw-tree-item {
+                > .cw-tree-item-wrapper {
+                    border-bottom: solid thin var(--content-color-40);
+                    display: block;
+
+                    > a.cw-tree-item-link {
+                        display: block;
+                        font-size: 18px;
+                        padding-left: 26px;
+                        @include background-icon(courseware, clickable, 18);
+                        background-repeat: no-repeat;
+                        background-position: 3px 3px;
+                    }
+                }
+
+                ol {
+                    padding-left: 0;
+
+                    > li.cw-tree-item {
+                        margin: 28px 0 0 0;
+
+                        > .cw-tree-item-wrapper {
+                            display: block;
+                            border-bottom: solid thin var(--content-color-40);
+                            margin-bottom: 12px;
+
+                            > a.cw-tree-item-link {
+                                display: inline-block;
+                                width: calc(100% - 4px);
+                                padding-left: 4px;
+                                font-size: 16px;
+
+                                &.cw-tree-item-link-edit {
+                                    width: calc(100% - 20px);
+                                }
+                            }
+                        }
+
+                        ol {
+                            padding-left: 0.25em;
+
+                            &.cw-tree-draggable-list {
+                                padding-left: 1em;
+                            }
+
+                            > li.cw-tree-item {
+                                margin: 4px 0;
+
+                                > .cw-tree-item-wrapper {
+                                    border: none;
+
+                                    > a.cw-tree-item-link {
+                                        display: inline-block;
+                                        border-bottom: none;
+                                        font-size: 14px;
+                                        width: calc(100% - 20px);
+                                        background-repeat: no-repeat;
+                                        padding-left: 18px;
+                                        margin-left: 4px;
+                                        margin-bottom: 0;
+
+                                        &.cw-tree-item-link-edit {
+                                            width: calc(100% - 38px);
+                                        }
+
+                                        @include background-icon(bullet-dot, clickable, 18);
+
+                                        &:hover {
+                                            @include background-icon(bullet-dot, attention, 18);
+                                        }
+
+                                        &.cw-tree-item-link-current {
+                                            @include background-icon(bullet-dot, info, 18);
+                                        }
+                                    }
+                                }
+
+                                ol {
+                                    padding-left: 1em;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    .cw-sortable-handle {
+        vertical-align: middle;
+    }
+
+    .cw-tree-item-link {
+        &:hover {
+            background-color: var(--light-gray-color-20);
+            color: var(--active-color);
+        }
+
+        &.cw-tree-item-link-current {
+            color: var(--black);
+            font-weight: 600;
+            cursor: default;
+
+            &::before {
+                color: var(--black);
+            }
+        }
+
+        &.cw-tree-item-link-selected {
+            font-style: italic;
+            font-weight: 600;
+        }
+
+        @each $type, $icon in $tree-item-flag-icons {
+            .cw-tree-item-flag-#{$type} {
+                display: inline-block;
+                width: 16px;
+                height: 16px;
+                vertical-align: top;
+                @include background-icon(#{$icon}, clickable, 16);
+            }
+
+            &:hover .cw-tree-item-flag-#{$type} {
+                @include background-icon(#{$icon}, attention, 16);
+            }
+
+            &.cw-tree-item-link-current .cw-tree-item-flag-#{$type} {
+                @include background-icon(#{$icon}, info, 16);
+            }
+        }
+    }
+
+    .cw-tree-item-sequential {
+        display: inline-block;
+        position: absolute;
+        right: 8px;
+
+        &.cw-tree-item-sequential-complete {
+            width: 16px;
+            height: 16px;
+            vertical-align: top;
+            @include background-icon(accept, info, 16);
+        }
+
+        &.cw-tree-item-sequential-percentage {
+            color: var(--black);
+            font-size: 14px;
+        }
+    }
+
+    .cw-tree-item-ghost {
+        opacity: 0.6;
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/shelf.scss b/resources/assets/stylesheets/scss/courseware/shelf.scss
new file mode 100644
index 0000000..a7b3eab
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/shelf.scss
@@ -0,0 +1,53 @@
+@use '../../mixins.scss' as *;
+
+.cw-contents-overview-teaser {
+    max-width: 782px;
+    background-color: var(--content-color-20);
+    background-image: url('#{$image-path}/courseware-keyvisual-negative.svg');
+    background-repeat: no-repeat;
+    background-size: 196px;
+    background-position-y: 50%;
+    background-position-x: 24px;
+    padding: 24px;
+    margin-bottom: 10px;
+
+    .cw-contents-overview-teaser-content {
+        padding-left: 220px;
+
+        header {
+            font-size: 1.5em;
+            margin-bottom: 0.5em;
+        }
+    }
+}
+
+.responsive-display {
+    .cw-contents-overview-teaser {
+        max-width: 782px;
+        background-size: 60%;
+        background-position-y: 24px;
+        background-position-x: 50%;
+        padding: 24px;
+        margin-bottom: 10px;
+
+        .cw-contents-overview-teaser-content {
+            padding-top: 28%;
+            padding-left: 0;
+            text-align: justify;
+
+            header {
+                font-size: 1.5em;
+                margin: 1em 0 0.5em 0;
+                text-align: center;
+            }
+        }
+    }
+}
+
+.cw-unit-items,
+.cw-shared-items {
+    margin-bottom: 10px;
+    h2 {
+        margin-top: 0;
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/sortable.scss b/resources/assets/stylesheets/scss/courseware/sortable.scss
new file mode 100644
index 0000000..107b3a8
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/sortable.scss
@@ -0,0 +1,57 @@
+@import '../drag-handle.scss';
+
+.cw-sortable-handle {
+    @extend .drag-handle;
+    display: inline-block;
+    height: 24px;
+    &.cw-sortable-handle-dragging {
+        cursor: grabbing;
+    }
+}
+
+.cw-block-item-sortable {
+    .cw-sortable-handle {
+        position: relative;
+        left: 4px;
+    }
+    .cw-block {
+        margin-top: -38px;
+    }
+}
+
+.cw-container-item-sortable {
+    .cw-sortable-handle {
+        position: relative;
+        left: 4px;
+    }
+    .cw-container {
+        margin-top: -38px;
+    }
+}
+
+.container-ghost,
+.block-ghost {
+    opacity: 0.6;
+}
+
+
+.cw-container-wrapper-edit {
+    .cw-structural-element-list {
+        width: 100%;
+        padding: 0;
+        margin-top: 8px;
+        list-style: none;
+    }
+}
+
+.cw-block-item-selected {
+    .cw-block-header {
+        font-style: italic;
+    }
+}
+
+.cw-container-item-selected {
+    .cw-container-header {
+        font-style: italic;
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/structural-element.scss b/resources/assets/stylesheets/scss/courseware/structural-element.scss
new file mode 100644
index 0000000..917684d
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/structural-element.scss
@@ -0,0 +1,273 @@
+@use '../../mixins.scss' as *;
+@import './variables.scss';
+
+#course-courseware-index,
+#contents-courseware-index,
+#course-courseware-courseware,
+#contents-courseware-courseware {
+    form.default input[type=date] {
+        max-width: 9em;
+    }
+}
+
+.cw-structural-element {
+    &.cw-structural-element-consumemode {
+        position: fixed;
+        top: 0;
+        left: 0;
+        height: 100%;
+        width: 100%;
+        padding: 0;
+        background-color: var(--white);
+        z-index: 1004;
+        overflow-y: scroll;
+        overflow-x: hidden;
+        scrollbar-width: thin;
+        scrollbar-color: var(--base-color) var(--dark-gray-color-5);
+    }
+
+    .cw-welcome-screen {
+        .cw-welcome-screen-keyvisual {
+            margin: 14px 0 42px 0;
+            width: 100%;
+            height: 400px;
+            background-image: url('#{$image-path}/courseware-keyvisual.svg');
+            background-repeat: no-repeat;
+            background-position: center center;
+        }
+
+        header {
+            padding: 0.5em 0;
+            text-align: center;
+            font-size: 2.25em;
+        }
+
+        .cw-welcome-screen-actions {
+            display: flex;
+            flex-wrap: wrap;
+            justify-content: center;
+        }
+    }
+
+    .cw-structural-element-discussion {
+        max-width: 1606px;
+        width: 100%;
+        margin-bottom: 1em;
+    }
+
+    .cw-container-wrapper {
+        max-width: $max-content-width;
+        margin: 0;
+        padding: 0;
+        display: flex;
+        flex-wrap: wrap;
+        align-items: stretch;
+        justify-content: space-between;
+
+        &.cw-container-wrapper-consume {
+            margin: 0 auto;
+            padding: 91px 15px 15px 15px;
+        }
+
+        &.cw-container-wrapper-discuss {
+            max-width: 1606px;
+        }
+    }
+
+    .cw-structural-element-description {
+        width: 400px;
+        height: 200px;
+        overflow-y: auto;
+        resize: none;
+    }
+
+    .cw-structural-element-color {
+        color: var(--white);
+        &.black {
+            background-color: map-get($tile-colors, 'black');
+        }
+        &.charcoal {
+            background-color: map-get($tile-colors, 'charcoal');
+        }
+        &.royal-purple {
+            background-color: map-get($tile-colors, 'royal-purple');
+        }
+        &.iguana-green {
+            background-color: map-get($tile-colors, 'iguana-green');
+        }
+        &.queen-blue {
+            background-color: map-get($tile-colors, 'queen-blue');
+        }
+        &.verdigris {
+            background-color: map-get($tile-colors, 'verdigris');
+        }
+        &.mulberry {
+            background-color: map-get($tile-colors, 'mulberry');
+        }
+        &.pumpkin {
+            background-color: map-get($tile-colors, 'pumpkin');
+        }
+        &.sunglow {
+            background-color: map-get($tile-colors, 'sunglow');
+        }
+        &.apple-green {
+            background-color: map-get($tile-colors, 'apple-green');
+        }
+
+        &.studip-blue {
+            background-color: map-get($tile-colors, 'studip-blue');
+        }
+        &.studip-lightblue {
+            background-color: map-get($tile-colors, 'studip-lightblue');
+        }
+        &.studip-red {
+            background-color: map-get($tile-colors, 'studip-red');
+        }
+        &.studip-green {
+            background-color: map-get($tile-colors, 'studip-green');
+        }
+        &.studip-yellow {
+            background-color: map-get($tile-colors, 'studip-yellow');
+        }
+        &.studip-gray {
+            background-color: map-get($tile-colors, 'studip-gray');
+        }
+    }
+
+    .cw-structural-element-info {
+        width: 600px;
+        tr:first-child {
+            width: 12em;
+            vertical-align: top;
+        }
+    }
+
+    .cw-companion-box-wrapper {
+        width: 100%;
+        max-width: $max-content-width;
+    }
+}
+
+.cw-structural-element-dialog {
+    input[type='text'] {
+        width: 20em;
+    }
+}
+
+.cw-structural-element-image-preview {
+    display: block;
+    max-height: 200px;
+    max-width: 400px;
+    width: auto;
+    height: auto;
+    margin: 0 auto;
+    padding-bottom: 1em;
+}
+
+.cw-structural-element-image-preview-placeholder {
+    width: 356px;
+    height: 200px;
+    margin: 0 auto;
+    background-color: var(--content-color-20);
+    background-size: 100% auto;
+    background-repeat: no-repeat;
+    background-position: center;
+    @include background-icon(courseware, clickable, 128);
+    margin-bottom: 1em;
+}
+
+.cw-element-permissions {
+    label {
+        display: block;
+        padding: 6px 0;
+    }
+    table.default {
+        > caption {
+            padding: 0;
+            font-size: 1.25em;
+        }
+        td.perm {
+            input.right,
+            input.date {
+                cursor: pointer !important;
+            }
+        }
+    }
+    button.cw-add-persons {
+        margin-left: 4px;
+    }
+    button.cw-permission-delete {
+        width: 24px;
+        height: 24px;
+        border: none;
+        background-color: transparent;
+        @include background-icon(trash, clickable);
+        background-repeat: no-repeat;
+        cursor: pointer;
+    }
+}
+
+.cw-element-export {
+    label {
+        input {
+            vertical-align: middle;
+        }
+
+        span {
+            vertical-align: middle;
+        }
+    }
+}
+
+.studip-dialog-with-tab {
+    .studip-dialog-body .studip-dialog-content {
+        padding: 0 4px;
+        .cw-tab-in-dialog {
+            .cw-tabs-nav {
+                border: none;
+                border-bottom: solid thin var(--content-color-40);
+                margin-bottom: 4px;
+            }
+            .cw-tabs-content {
+                border: none;
+                min-width: 500px;
+                min-height: 400px;
+                overflow-y: auto;
+                scrollbar-width: thin;
+                scrollbar-color: var(--base-color) var(--dark-gray-color-5);
+            }
+        }
+    }
+}
+
+@media only screen and (max-width: 1820px) {
+    .cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss {
+        max-width: $max-content-width;
+        .cw-container.cw-container-list.cw-container-item.cw-container-colspan-full {
+            .cw-default-block {
+                flex-flow: column;
+                .cw-discuss-wrapper {
+                    margin-left: 0;
+                    margin-top: 8px;
+                }
+            }
+        }
+    }
+}
+
+@media only screen and (max-width: 1200px) {
+    .cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss {
+        max-width: $max-content-width;
+        .cw-container.cw-container-list.cw-container-item.cw-container-colspan-half,
+        .cw-container.cw-container-list.cw-container-item.cw-container-colspan-half-center {
+            .cw-default-block {
+                flex-flow: column;
+                .cw-discuss-wrapper {
+                    margin-left: 0;
+                    margin-top: 8px;
+                    max-width: 540px;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/variables.scss b/resources/assets/stylesheets/scss/courseware/variables.scss
new file mode 100644
index 0000000..af97f92
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/variables.scss
@@ -0,0 +1,205 @@
+$element-icons: (
+    content: content2,
+    draft: category-draft,
+    task: category-task,
+    template: category-template,
+    oer: oer-campus,
+    other: category-others,
+    portfolio: category-portfolio
+);
+
+$tree-item-flag-icons: (
+    date: date,
+    write: edit,
+    cant-read: lock-locked2,
+);
+
+$tile-colors: (
+    black: #000,
+    charcoal: #3c454e,
+    royal-purple: #8656a2,
+    iguana-green: #66b570,
+    queen-blue: #536d96,
+    verdigris: #41afaa,
+    mulberry: #bf5796,
+    pumpkin: #f26e00,
+    sunglow: #ffca5c,
+    apple-green: #8bbd40,
+    studip-blue: #28497c,
+    studip-lightblue: #e7ebf1,
+    studip-red: #d60000,
+    studip-green: #008512,
+    studip-yellow: #ffbd33,
+    studip-gray: #636a71,
+);
+
+$icon-colors:(
+    icon-white: #ffffff,
+    icon-black: #000000,
+    icon-red: #cb1800,
+    icon-blue: #24437c,
+    icon-green: #00962d,
+    icon-gray: #6e6e6e,
+    icon-yellow: #ffad00
+);
+$border-colors:(
+    white: #ffffff,
+    black: #000000,
+    studip-red: #cb1800,
+    studip-blue: #24437c,
+    studip-green: #00962d,
+    studip-gray: #6e6e6e,
+    studip-yellow: #ffad00
+);
+
+$blockadder-items: (
+    before-after: block-comparison,
+    canvas: block-canvas,
+    gallery: block-gallery,
+    image-map: block-imagemap,
+    audio: audio,
+    chart: vote,
+    code: computer,
+    confirm: accept,
+    date: date,
+    dialog-cards: dialog-cards,
+    document: file-text,
+    download: download,
+    embed: code,
+    folder: folder-full,
+    headline: block-eyecatcher,
+    iframe: door-enter,
+    lti: plugin,
+    key-point: exclaim-circle,
+    link: link-extern,
+    table-of-contents: table-of-contents,
+    text: edit,
+    timeline: date-cycle,
+    typewriter: block-typewriter,
+    video: video2,
+    biography-achievements: medal,
+    biography-career: ranking,
+    biography-personal-information: own-license,
+    biography-goals: radar
+);
+
+$containeradder-items: (
+    accordion: block-accordion,
+    list: view-list,
+    tabs: block-tabs,
+);
+
+$cw-wrapper-gap: 0.5em;
+
+$icons: (
+    accept,
+    add,
+    add-circle,
+    admin,
+    aladdin,
+    arr_1down,
+    arr_1left,
+    arr_1right,
+    arr_1up,
+    arr_2down,
+    arr_2left,
+    arr_2right,
+    arr_2up,
+    audio,
+    audio3,
+    billboard,
+    block-canvas,
+    block-comparison,
+    block-eyecatcher,
+    block-gallery,
+    block-gallery2,
+    block-imagemap,
+    brainstorm,
+    campusnavi,
+    chat2,
+    code,
+    community2,
+    computer,
+    consultation,
+    content,
+    courseware,
+    crown,
+    date-single,
+    decline,
+    decline-circle,
+    doctoral_cap,
+    download,
+    dropbox,
+    edit,
+    exclaim,
+    exclaim-circle,
+    export,
+    favorite,
+    filter,
+    globe,
+    graph,
+    group2,
+    group3,
+    group4,
+    home2,
+    info,
+    info-circle,
+    install,
+    institute,
+    key,
+    knife,
+    learnmodule,
+    lightbulb,
+    lightbulb2,
+    link2,
+    link3,
+    link-extern,
+    link-intern,
+    literature,
+    lock-locked,
+    lock-unlocked,
+    mail2,
+    medal,
+    metro,
+    microphone,
+    module,
+    network,
+    notification,
+    notification2,
+    opencast,
+    outer-space,
+    permalink,
+    person,
+    phone,
+    picture,
+    place,
+    plugin,
+    question,
+    question-circle,
+    ranking,
+    remove,
+    remove-circle,
+    resources,
+    roles,
+    schedule2,
+    search,
+    settings,
+    span-empty,
+    span-1quarter,
+    span-2quarter,
+    span-3quarter,
+    span-full,
+    spiral,
+    sport,
+    staple,
+    star,
+    star-empty,
+    star-halffull,
+    test,
+    tools,
+    topic,
+    ufo,
+    video2,
+    visibility-visible,
+    wizard
+);
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/widgets.scss b/resources/assets/stylesheets/scss/courseware/widgets.scss
new file mode 100644
index 0000000..d5db20c
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/widgets.scss
@@ -0,0 +1,56 @@
+.cw-action-widget {
+    .cw-action-widget-show-toc {
+        @include background-icon(table-of-contents, clickable);
+    }
+    .cw-action-widget-edit {
+        @include background-icon(edit, clickable);
+    }
+    .cw-action-widget-sort {
+        @include background-icon(arr_1sort, clickable);
+    }
+    .cw-action-widget-add {
+        @include background-icon(add, clickable);
+    }
+    .cw-action-widget-info {
+        @include background-icon(info, clickable);
+    }
+    .cw-action-widget-star {
+        @include background-icon(star, clickable);
+    }
+    .cw-action-widget-trash {
+        @include background-icon(trash, clickable);
+    }
+    .cw-action-widget-oer {
+        @include background-icon(oer-campus, clickable);
+    }
+    .cw-action-widget-remove-lock {
+        @include background-icon(lock-unlocked, clickable);
+    }
+}
+
+.cw-export-widget {
+    .cw-export-widget-export {
+        @include background-icon(export, clickable);
+    }
+    .cw-export-widget-export-pdf {
+        @include background-icon(export, clickable);
+    }
+    .cw-export-widget-oer {
+        @include background-icon(share, clickable);
+    }
+}
+
+.cw-import-widget {
+    .cw-import-widget-archive {
+        @include background-icon(import, clickable);
+    }
+    .cw-import-widget-copy {
+        @include background-icon(copy, clickable);
+    }
+    .cw-import-widget-import {
+        @include background-icon(import, clickable);
+    }
+    .cw-action-widget-link {
+        @include background-icon(group, clickable);
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/wizards.scss b/resources/assets/stylesheets/scss/courseware/wizards.scss
new file mode 100644
index 0000000..953f510
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/wizards.scss
@@ -0,0 +1,57 @@
+.cw-element-selector-list {
+    list-style: none;
+    padding: 0;
+
+    .cw-element-selector-item {
+        display: block;
+        width: 100%;
+        border: solid thin var(--content-color-40);
+        padding: 0.5em;
+        margin-bottom: 5px;
+        background-color: var(--white);
+        color: var(--base-color);
+        text-align: left;
+        cursor: pointer;
+
+        &:hover {
+            color: var(--white);
+            background-color: var(--base-color);
+        }
+    }
+}
+
+form.default .courseware-structural-element-selector {
+    list-style: none;
+    padding-left: 0;
+
+    .courseware-structural-element-selector-item {
+        .radiobutton {
+            background: none;
+            border: none;
+            padding: 0;
+        }
+
+        a label {
+            cursor: pointer;
+        }
+
+        label {
+            display: inline-block;
+            margin-bottom: 0;
+            text-indent: 0;
+            vertical-align: middle;
+        }
+
+        img {
+            vertical-align: middle;
+            &.inactive {
+                opacity: 0.5;
+            }
+        }
+
+        ul {
+            list-style: none;
+            padding-left: 18px;
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware_gradients.scss b/resources/assets/stylesheets/scss/courseware_gradients.scss
deleted file mode 100644
index 97f8c7d..0000000
--- a/resources/assets/stylesheets/scss/courseware_gradients.scss
+++ /dev/null
@@ -1,150 +0,0 @@
-.faded-sun {
-    background: radial-gradient(
-            ellipse farthest-side at 76% 77%,
-            rgba(245, 228, 212, 0.25) 4%,
-            rgba(255, 255, 255, 0) calc(4% + 1px)
-        ),
-        radial-gradient(circle at 76% 40%, #fef6ec 4%, rgba(255, 255, 255, 0) 4.18%),
-        linear-gradient(135deg, #ff0000 0%, #000036 100%),
-        radial-gradient(ellipse at 28% 0%, #ffcfac 0%, rgba(98, 149, 144, 0.5) 100%),
-        linear-gradient(180deg, #cd6e8a 0%, #f5eab0 69%, #d6c8a2 70%, #a2758d 100%);
-    background-blend-mode: normal, normal, screen, overlay, normal;
-}
-
-.romantic-sun {
-    background: linear-gradient(180deg, #ffb7b7 0%, #727272 100%),
-        radial-gradient(60.91% 100% at 50% 0%, #ffd1d1 0%, #260000 100%),
-        linear-gradient(238.72deg, #ffdddd 0%, #720066 100%),
-        linear-gradient(127.43deg, #00ffff 0%, #ff4444 100%),
-        radial-gradient(100.22% 100% at 70.57% 0%, #ff0000 0%, #00ffe0 100%),
-        linear-gradient(127.43deg, #b7d500 0%, #3300ff 100%);
-    background-blend-mode: screen, overlay, hard-light, color-burn, color-dodge, normal;
-}
-
-.bright-rain {
-    background: radial-gradient(50% 123.47% at 50% 50%, #00ff94 0%, #720059 100%),
-        linear-gradient(121.28deg, #669600 0%, #ff0000 100%),
-        linear-gradient(360deg, #0029ff 0%, #8fff00 100%),
-        radial-gradient(100% 164.72% at 100% 100%, #6100ff 0%, #00ff57 100%),
-        radial-gradient(100% 148.07% at 0% 0%, #fff500 0%, #51d500 100%);
-    background-blend-mode: screen, color-dodge, overlay, difference, normal;
-}
-
-.soft-weather {
-    background: radial-gradient(80.99% 100% at 50% 0%, #00ff0a 0%, #36008e 100%),
-        radial-gradient(50% 123.47% at 50% 50%, #efe7c8 0%, #36008e 100%),
-        linear-gradient(301.28deg, #ff006b 0%, #48dd9e 100%),
-        linear-gradient(294.84deg, #5a60e4 0%, #d30000 100%),
-        linear-gradient(52.29deg, #000000 0%, #00ff85 100%),
-        radial-gradient(100% 138.69% at 100% 0%, #0007a5 0%, #ff7a00 100%),
-        radial-gradient(70.41% 100% at 50% 0%, #d5b300 0%, #2200aa 100%);
-    background-blend-mode: screen, screen, lighten, overlay, lighten, difference, normal;
-}
-
-.blue-bar {
-    background: linear-gradient(
-        123deg,
-        #e7ebf1 0%,
-        #e7ebf1 40%,
-        #d4dbe5 calc(40% + 1px),
-        #d4dbe5 60%,
-        #7e92b0 calc(60% + 1px),
-        #7e92b0 75%,
-        #28497c calc(75% + 1px),
-        #28497c 100%
-    );
-}
-.green-bar {
-    background: linear-gradient(
-        123deg,
-        #c5dea0 0%,
-        #c5dea0 40%,
-        #8bbd40 calc(40% + 1px),
-        #8bbd40 60%,
-        #129c94 calc(60% + 1px),
-        #129c94 75%,
-        #66b570 calc(75% + 1px),
-        #66b570 100%
-    );
-}
-
-.paper {
-    background-image: repeating-linear-gradient(0deg, #d8dadc 0px, #d8dadc 1px, transparent 1px, transparent 23px),
-        repeating-linear-gradient(90deg, #d8dadc 0px, #d8dadc 1px, transparent 1px, transparent 23px),
-        linear-gradient(#ffffff, #ffffff);
-}
-.blueprint {
-    background: #28497c;
-    background-image: 
-        radial-gradient(circle at center center, transparent 0%, #28497c 200%),
-        repeating-linear-gradient(0deg, #536d96 0px, #536d96 1px, transparent 1px, transparent 12px),
-        repeating-linear-gradient(90deg, #536d96 0px, #536d96 1px, transparent 1px, transparent 12px),
-        repeating-linear-gradient(0deg, #536d96 0px, #536d96 2px, transparent 1px, transparent 60px),
-        repeating-linear-gradient(90deg, #536d96 0px, #536d96 2px, transparent 1px, transparent 60px),
-        linear-gradient(#1f3f70, #1f3f70);
-}
-
-.color-blue {
-    background: #28497c;
-    background: -webkit-linear-gradient(90deg, #28497c, #28497c, #7e92b0);
-    background: linear-gradient(90deg, #28497c, #28497c, #7e92b0);
-}
-.color-purple {
-    background: #682c8b;
-    background: -webkit-linear-gradient(90deg, #682c8b, #682c8b, #a480b9);
-    background: linear-gradient(90deg, #682c8b, #682c8b, #a480b9);
-}
-.color-violet {
-    background: #af2d7b;
-    background: -webkit-linear-gradient(90deg, #af2d7b, #af2d7b, #cf81b0);
-    background: linear-gradient(90deg, #af2d7b, #af2d7b, #cf81b0);
-}
-.color-red {
-    background: #d60000;
-    background: -webkit-linear-gradient(90deg, #d60000, #d60000, #e76666);
-    background: linear-gradient(90deg, #d60000, #d60000, #e76666);
-}
-.color-orange {
-    background: #f26e00;
-    background: -webkit-linear-gradient(90deg, #f26e00, #f26e00, #f7a866);
-    background: linear-gradient(90deg, #f26e00, #f26e00, #f7a866);
-}
-.color-yellow {
-    background: #ffbd33;
-    background: -webkit-linear-gradient(90deg, #ffbd33, #ffbd33, #ffd785);
-    background: linear-gradient(90deg, #ffbd33, #ffbd33, #ffd785);
-}
-.color-green {
-    background: #6ead10;
-    background: -webkit-linear-gradient(90deg, #6ead10, #6ead10, #a8ce70);
-    background: linear-gradient(90deg, #6ead10, #6ead10, #a8ce70);
-}
-.color-petrol {
-    background: #129c94;
-    background: -webkit-linear-gradient(90deg, #129c94, #129c94, #70c3bf);
-    background: linear-gradient(90deg, #129c94, #129c94, #70c3bf);
-}
-.color-grey {
-    background: #3c454e;
-    background: -webkit-linear-gradient(90deg, #3c454e, #3c454e, #898f94);
-    background: linear-gradient(90deg, #3c454e, #3c454e, #898f94);
-}
-
-.grid {
-    background-image: radial-gradient(circle at center center, transparent 0%, #000000 95%),
-        repeating-linear-gradient(
-            45deg,
-            #3c454e 0,
-            #3c454e 1px,
-            transparent 1px,
-            transparent 7px
-        ),
-        repeating-linear-gradient(
-            135deg,
-            #3c454e 0,
-            #3c454e 1px,
-            transparent 1px,
-            transparent 14px
-        ),
-        linear-gradient(135deg, #000000, #000000);
-}
diff --git a/resources/assets/stylesheets/scss/progress_indicator.scss b/resources/assets/stylesheets/scss/progress_indicator.scss
index 55358f4..1bf0d1a 100644
--- a/resources/assets/stylesheets/scss/progress_indicator.scss
+++ b/resources/assets/stylesheets/scss/progress_indicator.scss
@@ -3,6 +3,10 @@
     display: flex;
     flex-direction: column;
 
+    &.loading-indicator-content {
+        margin-top: 76px;
+    }
+
     .progress-indicator {
         width: 100%;
         background-image: url("#{$image-path}/loading-indicator.svg");
diff --git a/resources/vue/components/blubber/Panel.vue b/resources/vue/components/blubber/Panel.vue
index 12f15a2..f70d1f6 100644
--- a/resources/vue/components/blubber/Panel.vue
+++ b/resources/vue/components/blubber/Panel.vue
@@ -1,6 +1,6 @@
 <template>
     <StudipProgressIndicator
-        class="cw-loading-indicator-content"
+        class="loading-indicator-content"
         :description="$gettext('Lade Kommentare...')"
         v-if="!doneFetching"
     />
diff --git a/resources/vue/components/courseware/ActivitiesApp.vue b/resources/vue/components/courseware/ActivitiesApp.vue
index 7bf8e23..cfbce77 100644
--- a/resources/vue/components/courseware/ActivitiesApp.vue
+++ b/resources/vue/components/courseware/ActivitiesApp.vue
@@ -12,8 +12,8 @@
 
 <script>
 import CoursewareActivities from './CoursewareActivities.vue';
-import CoursewareActivitiesWidgetFilterType from './CoursewareActivitiesWidgetFilterType.vue';
-import CoursewareActivitiesWidgetFilterUnit from './CoursewareActivitiesWidgetFilterUnit.vue';
+import CoursewareActivitiesWidgetFilterType from './widgets/CoursewareActivitiesWidgetFilterType.vue';
+import CoursewareActivitiesWidgetFilterUnit from './widgets/CoursewareActivitiesWidgetFilterUnit.vue';
 import { mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/AdminApp.vue b/resources/vue/components/courseware/AdminApp.vue
index d210558..4f2df7b 100644
--- a/resources/vue/components/courseware/AdminApp.vue
+++ b/resources/vue/components/courseware/AdminApp.vue
@@ -12,9 +12,9 @@
 </template>
 
 <script>
-import CoursewareAdminActionWidget from './CoursewareAdminActionWidget.vue';
+import CoursewareAdminActionWidget from './widgets/CoursewareAdminActionWidget.vue';
 import CoursewareAdminTemplates from './CoursewareAdminTemplates.vue';
-import CoursewareAdminViewWidget from './CoursewareAdminViewWidget.vue';
+import CoursewareAdminViewWidget from './widgets/CoursewareAdminViewWidget.vue';
 import { mapGetters, mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/CommentsApp.vue b/resources/vue/components/courseware/CommentsApp.vue
index 1716d4d..417423b 100644
--- a/resources/vue/components/courseware/CommentsApp.vue
+++ b/resources/vue/components/courseware/CommentsApp.vue
@@ -17,9 +17,9 @@
 <script>
 import CoursewareBlockCommentsOverview from './CoursewareBlockCommentsOverview.vue';
 import CoursewareStructuralElementCommentsOverview from './CoursewareStructuralElementCommentsOverview.vue';
-import CoursewareCommentsOverviewWidgetFilterType from './CoursewareCommentsOverviewWidgetFilterType.vue';
-import CoursewareCommentsOverviewWidgetFilterCreated from './CoursewareCommentsOverviewWidgetFilterCreated.vue';
-import CoursewareCommentsOverviewWidgetFilterUnit from './CoursewareCommentsOverviewWidgetFilterUnit.vue';
+import CoursewareCommentsOverviewWidgetFilterType from './widgets/CoursewareCommentsOverviewWidgetFilterType.vue';
+import CoursewareCommentsOverviewWidgetFilterCreated from './widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue';
+import CoursewareCommentsOverviewWidgetFilterUnit from './widgets/CoursewareCommentsOverviewWidgetFilterUnit.vue';
 
 import { mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/ContentReleasesApp.vue b/resources/vue/components/courseware/ContentReleasesApp.vue
index 1a2ed6f..68aed78 100644
--- a/resources/vue/components/courseware/ContentReleasesApp.vue
+++ b/resources/vue/components/courseware/ContentReleasesApp.vue
@@ -9,7 +9,7 @@
 <script>
 import CoursewareContentLinks from './CoursewareContentLinks.vue';
 import CoursewareContentShared from './CoursewareContentShared.vue';
-import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
+import CoursewareCompanionOverlay from './layouts/CoursewareCompanionOverlay.vue';
 
 export default {
     components: {
diff --git a/resources/vue/components/courseware/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/CoursewareAccordionContainer.vue
deleted file mode 100644
index f1ae440..0000000
--- a/resources/vue/components/courseware/CoursewareAccordionContainer.vue
+++ /dev/null
@@ -1,405 +0,0 @@
-<template>
-    <courseware-default-container
-        :container="container"
-        containerClass="cw-container-accordion"
-        :canEdit="canEdit"
-        :isTeacher="isTeacher"
-        @showEdit="setShowEdit"
-        @storeContainer="storeContainer"
-        @closeEdit="initCurrentData"
-    >
-        <template v-slot:containerContent>
-            <template v-if="showEditMode && canEdit && !currentElementisLink">
-                <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
-                <span id="operation" class="assistive-text">
-                    {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
-                </span>
-            </template>
-            <courseware-collapsible-box
-                v-for="(section, index) in currentSections"
-                :key="index"
-                :title="section.name"
-                :icon="section.icon"
-                :open="index === 0 || sortInSlots.includes(index)"
-            >
-                <ul v-if="!showEditMode || currentElementisLink" class="cw-container-accordion-block-list">
-                    <li v-for="block in section.blocks" :key="block.id" class="cw-block-item">
-                        <component
-                            :is="component(block)"
-                            :block="block"
-                            :canEdit="canEdit"
-                            :isTeacher="isTeacher"
-                        />
-                    </li>
-                </ul>
-                <template v-else>
-                    <template v-if="!processing">
-                        <draggable
-                            v-if="canEdit"
-                            class="cw-container-list-block-list cw-container-list-sort-mode"
-                            :class="[section.blocks.length === 0 ? 'cw-container-list-sort-mode-empty' : '']"
-                            tag="ol"
-                            role="listbox"
-                            v-model="section.blocks"
-                            v-bind="dragOptions"
-                            handle=".cw-sortable-handle"
-                            group="blocks"
-                            @start="isDragging = true"
-                            @end="dropBlock"
-                            :containerId="container.id"
-                            :sectionId="index"
-                        >
-                            <li v-for="block in section.blocks" :key="block.id" class="cw-block-item cw-block-item-sortable">
-                                <span
-                                    :class="{ 'cw-sortable-handle-dragging': isDragging }"
-                                    class="cw-sortable-handle"
-                                    tabindex="0"
-                                    role="button"
-                                    aria-describedby="operation"
-                                    :ref="'sortableHandle' + block.id"
-                                    @keydown="keyHandler($event, block.id, index)"
-                                ></span>
-                                <component
-                                    :is="component(block)"
-                                    :block="block"
-                                    :canEdit="canEdit"
-                                    :isTeacher="isTeacher"
-                                    :class="{ 'cw-block-item-selected': keyboardSelected === block.id}"
-                                    :blockId="block.id"
-                                />
-                            </li>
-                        </draggable>
-                        <template v-if="canAddElements">
-                            <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/>
-                        </template>
-                    </template>
-                    <div v-else class="progress-wrapper">
-                        <studip-progress-indicator :description="$gettext('Vorgang wird bearbeitet...')" />
-                    </div>
-                </template>
-            </courseware-collapsible-box>
-        </template>
-        <template v-slot:containerEditDialog>
-            <form class="default cw-container-dialog-edit-form" @submit.prevent="">
-                <fieldset v-for="(section, index) in currentContainer.attributes.payload.sections.filter(section => !section.locked)" :key="index">
-                    <label>
-                        <translate>Title</translate>
-                        <input type="text" v-model="section.name" />
-                    </label>
-                    <label>
-                        <translate>Icon</translate>
-                        <studip-select :options="icons" v-model="section.icon">
-                            <template #open-indicator="selectAttributes">
-                                <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
-                            </template>
-                            <template #no-options>
-                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
-                            </template>
-                            <template #selected-option="option">
-                                <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
-                            </template>
-                            <template #option="option">
-                                <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
-                            </template>
-                        </studip-select>
-                    </label>
-                    <label
-                        class="cw-container-section-delete"
-                        v-if="currentContainer.attributes.payload.sections.length > 1"
-                    >
-                    <button class="button trash" @click="deleteSection(index)"><translate>Fach löschen</translate></button>
-                    </label>
-                </fieldset>
-            </form>
-            <button class="button add" @click="addSection"><translate>Fach hinzufügen</translate></button>
-        </template>
-    </courseware-default-container>
-</template>
-
-<script>
-import ContainerComponents from './container-components.js';
-import containerMixin from '../../mixins/courseware/container.js';
-import contentIcons from './content-icons.js';
-import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
-import StudipIcon from './../StudipIcon.vue';
-
-import { mapGetters, mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-accordion-container',
-    mixins: [containerMixin],
-    components: Object.assign(ContainerComponents, {
-        CoursewareCollapsibleBox,
-        StudipIcon,
-    }),
-    props: {
-        container: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-        canAddElements: Boolean,
-    },
-    data() {
-        return {
-            showEdit: false,
-            currentContainer: {},
-            currentSections: [],
-            unallocatedBlocks: [],
-            sortMode: false,
-            isDragging: false,
-            dragOptions: {
-                animation: 0,
-                group: this.container.id,
-                disabled: false,
-                ghostClass: "block-ghost"
-            },
-            processing: false,
-            keyboardSelected: null,
-            sortInSlots: [],
-            assistiveLive: ''
-        };
-    },
-    computed: {
-        ...mapGetters({
-            blockById: 'courseware-blocks/byId',
-            viewMode: 'viewMode',
-            currentElementisLink: 'currentElementisLink'
-        }),
-        showEditMode() {
-            return this.viewMode === 'edit';
-        },
-        blocks() {
-            if (!this.container) {
-                return [];
-            }
-
-            return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })).filter((a) => a);
-        },
-        icons() {
-            return contentIcons;
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateContainer: 'updateContainer',
-            loadContainer: 'courseware-containers/loadById',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-        }),
-        initCurrentData() {
-            this.currentContainer = _.cloneDeep(this.container);
-
-            let view = this;
-            let sections = this.currentContainer.attributes.payload.sections;
-
-            const unallocated = new Set(this.blocks.map(({ id }) => id));
-
-            for (let section of sections) {
-                section.locked = false;
-                section.blocks = section.blocks.map((id) =>  view.blockById({id})).filter(Boolean);
-                for (let sectionBlock of section.blocks) {
-                    if (sectionBlock?.id && unallocated.has(sectionBlock.id)) {
-                        unallocated.delete(sectionBlock.id);
-                    }
-                }
-            }
-
-            if (unallocated.size > 0) {
-                this.unallocatedBlocks = [...unallocated].map((id) => view.blockById({ id }));
-                sections.push({
-                    blocks: this.unallocatedBlocks,
-                    name: this.$gettext('nicht zugewiesene Inhalte'),
-                    icon: 'decline',
-                    locked: true
-                });
-            }
-
-            this.currentSections = sections;
-            this.sortInSlots = [];
-        },
-        setShowEdit(state) {
-            this.showEdit = state;
-        },
-        addSection() {
-            this.currentContainer.attributes.payload.sections.push({ name: '', icon: '', blocks: [] });
-        },
-        deleteSection(index) {
-            if (this.currentContainer.attributes.payload.sections.length === 1) {
-                return;
-            }
-            if (this.currentContainer.attributes.payload.sections[index].blocks.length > 0) {
-                if (index === 0) {
-                    this.currentContainer.attributes.payload.sections[
-                        index + 1
-                    ].blocks = this.currentContainer.attributes.payload.sections[index + 1].blocks.concat(
-                        this.currentContainer.attributes.payload.sections[index].blocks
-                    );
-                } else {
-                    this.currentContainer.attributes.payload.sections[
-                        index - 1
-                    ].blocks = this.currentContainer.attributes.payload.sections[index - 1].blocks.concat(
-                        this.currentContainer.attributes.payload.sections[index].blocks
-                    );
-                }
-            }
-            this.currentContainer.attributes.payload.sections.splice(index, 1);
-        },
-        async storeContainer() {
-            const timeout = setTimeout(() => this.processing = true, 800);
-            this.currentContainer.attributes.payload.sections = this.currentContainer.attributes.payload.sections.filter(section => !section.locked);
-            this.currentContainer.attributes.payload.sections.forEach(section => {
-                section.blocks = section.blocks.map((block) => {return block.id;});
-                delete section.locked;
-            });
-            await this.updateContainer({
-                container: this.currentContainer,
-                structuralElementId: this.currentContainer.relationships['structural-element'].data.id,
-            });
-            await this.unlockObject({ id: this.currentContainer.id, type: 'courseware-containers' });
-            await this.loadContainer({id : this.container.id });
-            this.initCurrentData();
-            clearTimeout(timeout);
-            this.processing = false;
-        },
-        async storeSort() {
-            if (this.blockedByAnotherUser) {
-                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
-                this.loadContainer({id : this.container.id });
-                return false;
-            }
-            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-            await this.storeContainer();
-            this.assistiveLive = '';
-        },
-
-        component(block) {
-            if (block.attributes) {
-                return 'courseware-' + block.attributes["block-type"] + '-block';
-            }
-            return null;
-        },
-        updateContent(blockAdder) {
-            if (blockAdder.container !== undefined && blockAdder.container.id === this.container.id) {
-                this.initCurrentData();
-            }
-        },
-        keyHandler(e, blockId, sectionIndex) {
-            switch (e.keyCode) {
-                case 27: // esc
-                    this.abortKeyboardSorting(blockId, sectionIndex);
-                    break;
-                case 13: // enter
-                    e.preventDefault();
-                    if (this.keyboardSelected) {
-                        this.storeKeyboardSorting(blockId, sectionIndex);
-                    } else {
-                        this.keyboardSelected = blockId;
-                        const block = this.blockById({id: blockId});
-                        const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
-                        this.assistiveLive =
-                            this.$gettextInterpolate(
-                                this.$gettext('%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
-                                , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                            );
-                    }
-                    break;
-            }
-            if (this.keyboardSelected) {
-                switch (e.keyCode) {
-                    case 9: //tab
-                        this.abortKeyboardSorting(blockId, sectionIndex);
-                        break;
-                    case 38: // up
-                        e.preventDefault();
-                        this.moveItemUp(blockId, sectionIndex);
-                        break;
-                    case 40: // down
-                        e.preventDefault();
-                        this.moveItemDown(blockId, sectionIndex);
-                        break;
-                }
-            }
-        },
-        moveItemUp(blockId, sectionIndex) {
-            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
-            const block = this.blockById({id: blockId});
-            if (currentIndex !== 0) {
-                const newPos = currentIndex - 1;
-                this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-                this.assistiveLive =
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                    );
-            } else if (sectionIndex !== 0) {
-                const newSectionIndex = sectionIndex - 1;
-                if (!this.sortInSlots.includes(newSectionIndex)) {
-                    this.sortInSlots.push(newSectionIndex);
-                }
-                this.currentSections[newSectionIndex].blocks.push(this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-            }
-        },
-        moveItemDown(blockId, sectionIndex) {
-            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
-            const block = this.blockById({id: blockId});
-            if (this.currentSections[sectionIndex].blocks.length - 1 > currentIndex) {
-                const newPos = currentIndex + 1;
-                this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-                this.assistiveLive =
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                    );
-            } else if (this.currentSections.length - 1 > sectionIndex) {
-                const newSectionIndex = sectionIndex + 1;
-                if (!this.sortInSlots.includes(newSectionIndex)) {
-                    this.sortInSlots.push(newSectionIndex);
-                }
-                this.currentSections[newSectionIndex].blocks.splice(0, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-            }
-        },
-        abortKeyboardSorting(blockId, sectionIndex) {
-            const block = this.blockById({id: blockId});
-            this.keyboardSelected = null;
-            this.assistiveLive =
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, Neuordnung abgebrochen.')
-                    , {blockTitle: block.attributes.title}
-                );
-            this.initCurrentData();
-        },
-        storeKeyboardSorting(blockId, sectionIndex) {
-            const block = this.blockById({id: blockId});
-            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
-            this.keyboardSelected = null;
-            this.assistiveLive =
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.')
-                    , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                );
-            this.storeSort();
-        }
-    },
-    watch: {
-        blocks(newBlocks, oldBlocks) {
-            if (!this.showEdit && !this.checkSimpleArrayEquality(newBlocks, oldBlocks)) {
-                this.$nextTick(() => {
-                    setTimeout(() =>  this.initCurrentData(), 250);
-                });
-            }
-        },
-        currentSections: {
-            handler() {
-                if (this.keyboardSelected) {
-                    this.$nextTick(() => {
-                        this.$refs['sortableHandle' + this.keyboardSelected][0].focus();
-                    });
-                }
-            },
-            deep: true
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareActionWidget.vue b/resources/vue/components/courseware/CoursewareActionWidget.vue
deleted file mode 100644
index 08348b9..0000000
--- a/resources/vue/components/courseware/CoursewareActionWidget.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-    <sidebar-widget id="courseware-action-widget" :title="$gettext('Aktionen')" v-if="structuralElement">
-        <template #content>
-            <ul class="widget-list widget-links cw-action-widget">
-                <li v-if="canEdit" class="cw-action-widget-add">
-                    <button @click="addElement">
-                        {{ $gettext('Seite hinzufügen') }}
-                    </button>
-                </li>
-            </ul>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-action-widget',
-    props: ['structuralElement'],
-    components: {
-        SidebarWidget,
-    },
-    computed: {
-        canEdit() {
-            if (!this.structuralElement) {
-                return false;
-            }
-            return this.structuralElement.attributes['can-edit'];
-        },
-    },
-    methods: {
-        ...mapActions({
-            showElementAddDialog: 'showElementAddDialog',
-        }),
-        addElement() {
-            this.showElementAddDialog(true);
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareActivities.vue b/resources/vue/components/courseware/CoursewareActivities.vue
index aab705f..a236842 100644
--- a/resources/vue/components/courseware/CoursewareActivities.vue
+++ b/resources/vue/components/courseware/CoursewareActivities.vue
@@ -20,7 +20,7 @@
 
 <script>
 import CoursewareActivityItem from './CoursewareActivityItem.vue';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+import CoursewareCompanionBox from './layouts/CoursewareCompanionBox.vue';
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
 
 import { mapActions, mapGetters } from 'vuex';
diff --git a/resources/vue/components/courseware/CoursewareActivitiesWidgetFilterType.vue b/resources/vue/components/courseware/CoursewareActivitiesWidgetFilterType.vue
deleted file mode 100644
index 0c57271..0000000
--- a/resources/vue/components/courseware/CoursewareActivitiesWidgetFilterType.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<template>
-    <sidebar-widget :title="$gettext('Aktivitäten')">
-        <template #content>
-            <div class="cw-filter-widget">
-                <form class="default" @submit.prevent="">
-                    <select v-model="typeFilter">
-                        <option value="all">
-                            {{ $gettext('Alle') }}
-                        </option>
-                        <option value="edited">
-                            {{ $gettext('Bearbeitet') }}
-                        </option>
-                        <option value="created">
-                            {{ $gettext('Erstellt') }}
-                        </option>
-                        <option value="answered">
-                            {{ $gettext('Feedback') }}
-                        </option>
-                        <option value="interacted">
-                            {{ $gettext('Kommentiert') }}
-                        </option>
-                        <option value="voided">
-                            {{ $gettext('Gelöscht') }}
-                        </option>
-                    </select>
-                </form>
-            </div>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-activities-widget-filter-type',
-    components: {
-        SidebarWidget
-    },
-    data() {
-        return {
-            typeFilter: 'all',
-        };
-    },
-    methods: {
-        ...mapActions({
-            setTypeFilter: 'setTypeFilter',
-        }),
-        filterType() {
-            this.setTypeFilter(this.typeFilter);
-        },
-    },
-    watch: {
-        typeFilter() {
-            this.filterType();
-        },
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareActivitiesWidgetFilterUnit.vue b/resources/vue/components/courseware/CoursewareActivitiesWidgetFilterUnit.vue
deleted file mode 100644
index d0142ea..0000000
--- a/resources/vue/components/courseware/CoursewareActivitiesWidgetFilterUnit.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<template>
-    <sidebar-widget :title="$gettext('Lernmaterial')">
-        <template #content>
-            <div class="cw-filter-widget">
-                <form class="default" @submit.prevent="">
-                    <select v-model="unitFilter">
-                        <option value="all">
-                            {{ $gettext('Alle') }}
-                        </option>
-                        <option v-for="unit in coursewareUnits" :key="unit.id" :value="unit.id">
-                                {{ getUnitTitle(unit) }}
-                        </option>
-                    </select>
-                </form>
-            </div>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-activities-widget-filter-unit',
-    components: {
-        SidebarWidget
-    },
-    data() {
-        return {
-            unitFilter: 'all'
-        };
-    },
-    computed: {
-        ...mapGetters({
-            getStructuralElementById: 'courseware-structural-elements/byId',
-            coursewareUnits: 'courseware-units/all',
-        }),
-    },
-    methods: {
-        ...mapActions({
-            setUnitFilter: 'setUnitFilter',
-        }),
-        filterUnit() {
-            this.setUnitFilter(this.unitFilter);
-        },
-        getUnitTitle(unit) {
-            return this.getStructuralElementById({id: unit.relationships['structural-element'].data.id }).attributes.title;
-        }
-    },
-    watch: {
-        unitFilter() {
-            this.filterUnit();
-        }
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareAdminActionWidget.vue b/resources/vue/components/courseware/CoursewareAdminActionWidget.vue
deleted file mode 100644
index 6c02699..0000000
--- a/resources/vue/components/courseware/CoursewareAdminActionWidget.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
-    <ul class="widget-list widget-links cw-action-widget">
-        <li v-if="templatesView" class="cw-action-widget-add">
-            <a href="#"  @click.prevent="addTemplate">
-                <translate>Vorlage hinzufügen</translate>
-            </a>
-        </li>
-    </ul>
-</template>
-
-<script>
-import { mapGetters, mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-admin-action-widget',
-    computed: {
-        ...mapGetters({
-            adminViewMode: 'adminViewMode',
-            showAddTemplateDialog: 'showAddTemplateDialog'
-        }),
-        templatesView() {
-            return this.adminViewMode === 'templates';
-        }
-    },
-    methods: {
-        ...mapActions({
-            setShowAddTemplateDialog: 'showAddTemplateDialog'
-        }),
-        addTemplate() {
-            this.setShowAddTemplateDialog(true);
-        }
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareAdminViewWidget.vue b/resources/vue/components/courseware/CoursewareAdminViewWidget.vue
deleted file mode 100644
index 81a8071..0000000
--- a/resources/vue/components/courseware/CoursewareAdminViewWidget.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<template>
-  <ul class="widget-list widget-links sidebar-views cw-view-widget">
-        <li :class="{ active: templatesView }">
-            <a href="#" @click.prevent="setTemplatesView">
-                <translate>Vorlagen</translate>
-            </a>
-        </li>
-  </ul>
-</template>
-
-<script>
-import { mapGetters, mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-admin-view-widget',
-    computed: {
-        ...mapGetters({
-            adminViewMode: 'adminViewMode'
-        }),
-        templatesView() {
-            return this.adminViewMode === 'templates';
-        },
-    },
-    methods: {
-        ...mapActions({
-            setAdminViewMode: 'adminViewMode'
-        }),
-        setTemplatesView() {
-            this.setAdminViewMode('templates');
-        },
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareAudioBlock.vue b/resources/vue/components/courseware/CoursewareAudioBlock.vue
deleted file mode 100644
index e7f47c0..0000000
--- a/resources/vue/components/courseware/CoursewareAudioBlock.vue
+++ /dev/null
@@ -1,636 +0,0 @@
-<template>
-    <div class="cw-block cw-block-audio">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
-                <audio
-                    :src="currentURL"
-                    class="cw-audio-player"
-                    ref="audio"
-                    @timeupdate="onTimeUpdateListener"
-                    @durationchange="setDuration"
-                    @ended="onEndedListener"
-                />
-                <div v-if="!emptyAudio" class="cw-audio-container">
-                    <div class="cw-audio-current-track">
-                        <p>{{ activeTrackName }}</p>
-                    </div>
-                    <div class="cw-audio-controls">
-                        <input
-                            class="cw-audio-range"
-                            ref="range"
-                            type="range"
-                            :value="currentSeconds"
-                            min="0"
-                            :max="Math.round(durationSeconds)"
-                            @input="rangeAction"
-                        />
-                        <span class="cw-audio-time">{{ currentTime }} {{ durationTime ? '/ ' + durationTime : '' }}</span>
-
-                        <button v-if="hasPlaylist" class="cw-audio-button cw-audio-prevbutton" :title="$gettext('Zurück')" @click="prevAudio" />
-                        <button v-if="!playing" class="cw-audio-button cw-audio-playbutton" :title="$gettext('Abspielen')" @click="playAudio" />
-                        <button v-if="playing" class="cw-audio-button cw-audio-pausebutton" :title="$gettext('Pause')" @click="pauseAudio" />
-                        <button v-if="hasPlaylist" class="cw-audio-button cw-audio-nextbutton" :title="$gettext('Weiter')" @click="nextAudio" />
-                        <button class="cw-audio-button cw-audio-stopbutton" :title="$gettext('Anhalten')" @click="stopAudio" />
-                    </div>
-                </div>
-                <div v-if="emptyAudio" class="cw-audio-empty">
-                    <p>{{ $gettext('Es ist keine Audio-Datei verfügbar') }}</p>
-                </div>
-                <div v-show="currentSource === 'studip_folder'" class="cw-audio-playlist-wrapper" :class="[!showRecorder && emptyAudio ? 'empty' : '']">
-                    <ul v-show="hasPlaylist" class="cw-audio-playlist" :class="[showRecorder ? 'with-recorder' : '']">
-                        <li v-for="(file, index) in files" :key="file.id">
-                            <a
-                                :aria-current="(index === currentPlaylistItem) ? 'true' : 'false'"
-                                :class="{
-                                    'is-playing': index === currentPlaylistItem && playing,
-                                    'current-item': index === currentPlaylistItem,
-                                }"
-                                :title="$gettext('Audiodatei:') + ' ' + file.name"
-                                href="#"
-                                class="cw-playlist-item"
-                                @click.prevent="setCurrentPlaylistItem(index)"
-                            >
-                                {{ file.name }}
-                            </a>
-                        </li>
-                    </ul>
-                    <div v-if="showRecorder && canGetMediaDevices" class="cw-audio-playlist-recorder">
-                        <button 
-                            v-show="!userRecorderEnabled"
-                            class="button"
-                            :disabled="!folderSelected || folderLoadError"
-                            :title="enableRecorderTitle"
-                            @click="enableRecorder"
-                        >
-                            {{ $gettext('Aufnahme aktivieren') }}
-                        </button>
-                        <button
-                            v-show="userRecorderEnabled && !isRecording && !newRecording"
-                            class="button"
-                            @click="startRecording"
-                        >
-                            {{ $gettext('Aufnahme starten') }}
-                        </button>
-                        <button
-                            v-show="newRecording && !isRecording"
-                            class="button"
-                            @click="startRecording"
-                        >
-                            {{ $gettext('Aufnahme wiederholen') }}
-                        </button>
-                        <button 
-                            v-show="isRecording"
-                            class="button"
-                            @click="stopRecording"
-                        >
-                            {{ $gettext('Aufnahme beenden') }}
-                        </button>
-                        <button 
-                            v-show="newRecording && !isRecording"
-                            class="button"
-                            @click="resetRecorder"
-                        >
-                            {{ $gettext('Aufnahme löschen') }}
-                        </button>
-                        <button 
-                            v-show="newRecording && !isRecording"
-                            class="button"
-                            @click="storeRecording"
-                        >
-                            {{ $gettext('Aufnahme speichern') }}
-                        </button>
-                        <span v-show="isRecording">
-                            {{ $gettext('Aufnahme läuft') }}: {{seconds2time(timer)}}
-                        </span>
-                    </div>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Überschrift') }}
-                        <input type="text" v-model="currentTitle" />
-                    </label>
-                    <label>
-                        {{ $gettext('Quelle') }}
-                        <select v-model="currentSource">
-                            <option value="studip_file">{{ $gettext('Dateibereich Datei') }}</option>
-                            <option value="studip_folder">{{ $gettext('Dateibereich Ordner') }}</option>
-                            <option value="web">{{ $gettext('Web-Adresse') }}</option>
-                        </select>
-                    </label>
-                    <label v-show="currentSource === 'web'">
-                        {{ $gettext('URL') }}
-                        <input type="text" v-model="currentWebUrl" />
-                    </label>
-                    <label v-show="currentSource === 'studip_file'">
-                        {{ $gettext('Datei') }}
-                        <courseware-file-chooser
-                            v-model="currentFileId"
-                            :isAudio="true"
-                            @selectFile="updateCurrentFile"
-                        />
-                    </label>
-                    <label v-show="currentSource === 'studip_folder'">
-                        {{ $gettext('Ordner') }}
-                        <courseware-folder-chooser v-model="currentFolderId" allowUserFolders />
-                    </label>
-                    <label v-show="currentSource === 'studip_folder'">
-                        {{ $gettext('Audio Aufnahmen zulassen') }}
-                        <span
-                            class="tooltip tooltip-icon"
-                            :data-tooltip="$gettext('Um Aufnahmen zu ermöglichen, muss ein Ordner ausgewählt werden.')"
-                        ></span>
-                        <select v-model="currentRecorderEnabled" :disabled="!folderSelected">
-                            <option :value="true">{{ $gettext('Ja') }}</option>
-                            <option :value="false">{{ $gettext('Nein') }}</option>
-                        </select>
-                    </label>
-                </form>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Audio-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFileChooser from './CoursewareFileChooser.vue';
-import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-audio-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareFileChooser,
-        CoursewareFolderChooser,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentSource: '',
-            currentFileId: '',
-            currentFolderId: '',
-            currentWebUrl: '',
-            currentFile: {},
-            currentSeconds: 0,
-            durationSeconds: 0,
-            playing: false,
-            currentPlaylistItem: 0,
-            currentRecorderEnabled: false,
-            userRecorderEnabled: false,
-            recorder: null,
-            chunks: [],
-            timer: 0,
-            isRecording: false,
-            newRecording: false,
-            folderLoadError: false
-        };
-    },
-    computed: {
-        ...mapGetters({
-            fileRefById: 'file-refs/byId',
-            relatedFileRefs: 'file-refs/related',
-            urlHelper: 'urlHelper',
-            userId: 'userId',
-            usersById: 'users/byId',
-            relatedTermOfUse: 'terms-of-use/related'
-        }),
-        files() {
-            const files =
-                this.relatedFileRefs({
-                    parent: { type: 'folders', id: this.currentFolderId },
-                    relationship: 'file-refs'
-                }) ?? [];
-
-            return files
-                .filter((file) => {
-                    if (this.relatedTermOfUse({parent: file, relationship: 'terms-of-use'}).attributes['download-condition'] !== 0) {
-                        return false;
-                    } 
-                    if (! file.attributes['mime-type'].includes('audio')) {
-                        return false;
-                    }
-
-                    return true;
-                })
-                .map(({ id, attributes }) => {
-                    return {
-                        id,
-                        name: attributes.name,
-                        download_url: this.urlHelper.getURL(
-                            'sendfile.php/',
-                            { type: 0, file_id: id, file_name: attributes.name },
-                            true
-                        ),
-                        mime_type: attributes['mime-type']
-                    };
-                });
-        },
-        currentTime() {
-            return this.seconds2time(this.currentSeconds);
-        },
-        durationTime() {
-            if (this.durationSeconds > 0) {
-                return this.seconds2time(this.durationSeconds);
-            }
-            return false;
-        },
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        source() {
-            return this.block?.attributes?.payload?.source;
-        },
-        fileId() {
-            return this.block?.attributes?.payload?.file_id;
-        },
-        folderId() {
-            return this.block?.attributes?.payload?.folder_id;
-        },
-        folderSelected() {
-            return this.currentFolderId !== '';
-        },
-        webUrl() {
-            return this.block?.attributes?.payload?.web_url;
-        },
-        recorderEnabled() {
-            return this.block?.attributes?.payload?.recorder_enabled;
-        },
-        showRecorder() {
-            return this.currentRecorderEnabled && this.currentSource === 'studip_folder';
-        },
-        hasPlaylist() {
-            return this.files.length > 0 && this.currentSource === 'studip_folder';
-        },
-        canGetMediaDevices() {
-            return navigator.mediaDevices !== undefined;
-        },
-        currentURL() {
-            if (this.currentSource === 'studip_file') {
-                return this.currentFile.download_url;
-            }
-            if (this.currentSource === 'studip_folder') {
-                if (this.files.length > 0) {
-                    return this.files[this.currentPlaylistItem].download_url;
-                } else {
-                    return '';
-                }
-            }
-            if (this.currentSource === 'web') {
-                return this.currentWebUrl;
-            }
-
-            return '';
-        },
-        activeTrackName() {
-            if (this.currentSource === 'studip_file') {
-                return this.currentFile.name;
-            }
-            if (this.currentSource === 'studip_folder') {
-                if (this.files.length > 0) {
-                    return this.files[this.currentPlaylistItem].name;
-                } else {
-                    return '';
-                }
-            }
-            if (this.currentSource === 'web') {
-                return this.currentWebUrl;
-            }
-
-            return '';
-        },
-        emptyAudio() {
-            if (this.currentSource === 'studip_folder' && this.currentFolderId !== '' && this.files.length > 0) {
-                return false;
-            }
-            if (this.currentSource === 'studip_file' && this.currentFileId !== '') {
-                return false;
-            }
-            if (this.currentSource === 'web' && this.currentWebUrl !== '') {
-                return false;
-            }
-            return true;
-        },
-        enableRecorderTitle() {
-            if (!this.folderSelected) {
-                return this.$gettext('Aufnahme nicht möglich, es wurde kein Ordner ausgewählt.');
-            }
-
-            if (this.folderLoadError) {
-                return this.$gettext('Aufnahme nicht möglich, der ausgewählte Ordner konnte nicht gefunden werden.');
-            }
-
-            return this.$gettext('Aktiviert die Aufnahmefunktion');
-        }
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            loadFileRef: 'file-refs/loadById',
-            loadRelatedFileRefs: 'file-refs/loadRelated',
-            updateBlock: 'updateBlockInContainer',
-            companionWarning: 'companionWarning',
-            companionSuccess: 'companionSuccess',
-            companionError: 'companionError',
-            createFile: 'createFile',
-        }),
-        initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentSource = this.source;
-            this.currentFileId = this.fileId;
-            this.currentWebUrl = this.webUrl;
-            if (this.currentFileId !== '') {
-                this.loadFile();
-            }
-            this.currentFolderId = this.folderId;
-            this.currentRecorderEnabled = this.recorderEnabled;
-        },
-        updateCurrentFile(file) {
-            this.currentFile = file;
-            this.currentFileId = file.id;
-        },
-        async getFolderFiles() {
-            try {
-                await this.loadRelatedFileRefs({
-                    parent: { type: 'folders', id: this.currentFolderId },
-                    relationship: 'file-refs',
-                    options: { include: 'terms-of-use' }
-                });
-            } catch(error) {
-                this.folderLoadError = true;
-            }
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.title = this.currentTitle;
-            attributes.payload.source = this.currentSource;
-            attributes.payload.file_id = '';
-            attributes.payload.web_url = '';
-            attributes.payload.folder_id = '';
-            attributes.payload.recorder_enabled = false;
-            if (this.currentSource === 'studip_file') {
-                if (this.currentFileId === '') {
-                    this.companionWarning({
-                        info: this.$gettext('Bitte wählen Sie eine Datei aus.')
-                    });
-                    return false;
-                }
-                attributes.payload.file_id = this.currentFileId;
-            } else if (this.currentSource === 'web') {
-                attributes.payload.web_url = this.currentWebUrl;
-            } else if (this.currentSource === 'studip_folder') {
-                if (this.currentFolderId === '') {
-                    this.companionWarning({
-                        info: this.$gettext('Bitte wählen Sie einen Ordner aus.')
-                    });
-                    return false;
-                }
-                attributes.payload.folder_id = this.currentFolderId;
-                attributes.payload.recorder_enabled = this.currentRecorderEnabled;
-            } else {
-                return false;
-            }
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-        rangeAction() {
-            if (this.$refs.range.value !== this.currentSeconds) {
-                this.$refs.audio.currentTime = this.$refs.range.value;
-            }
-        },
-        setDuration() {
-            let duration = this.$refs.audio.duration
-            if (!isNaN(duration) && isFinite(duration)) {
-                this.durationSeconds = duration;
-            } else {
-                this.durationSeconds = 0;
-            }
-        },
-        playAudio() {
-            const audio = this.$refs.audio;
-            let isSupported = 'unknown';
-            if (this.currentSource === 'studip_file') {
-                isSupported = audio.canPlayType(this.currentFile.mime_type);
-            }
-            if (this.currentSource === 'studip_folder') {
-                isSupported = audio.canPlayType(this.files[this.currentPlaylistItem].mime_type);
-            }
-            if (isSupported !== '') {
-                audio.play();
-                this.playing = true;
-            } else {
-                this.companionError({
-                    info: this.$gettext('Ihr Browser unterstützt dieses Audioformat leider nicht.')
-                });
-                if(this.hasPlaylist) {
-                    this.nextAudio();
-                }
-            }
-        },
-        pauseAudio() {
-            this.$refs.audio.pause();
-            this.playing = false;
-        },
-        stopAudio() {
-            this.pauseAudio();
-            this.$refs.audio.currentTime = 0;
-        },
-        onTimeUpdateListener() {
-            this.currentSeconds = this.$refs.audio.currentTime;
-        },
-        onEndedListener() {
-            this.stopAudio();
-            if(this.hasPlaylist) {
-                this.nextAudio();
-            }
-        },
-        seconds2time(seconds) {
-            seconds = Math.round(seconds);
-            let hours = Math.floor(seconds / 3600);
-            let minutes = Math.floor((seconds - hours * 3600) / 60);
-            let time = '';
-            seconds = seconds - hours * 3600 - minutes * 60;
-            if (hours !== 0) {
-                time = hours + ':';
-            }
-            if (minutes !== 0 || time !== '') {
-                minutes = minutes < 10 && time !== '' ? '0' + minutes : String(minutes);
-                time += minutes + ':';
-            }
-            if (time === '') {
-                time = seconds < 10 ? '0:0' + seconds : '0:' + seconds;
-            } else {
-                time += seconds < 10 ? '0' + seconds : String(seconds);
-            }
-            return time;
-        },
-        setCurrentPlaylistItem(index) {
-            if (this.currentPlaylistItem === index) {
-                if (this.playing) {
-                    this.pauseAudio();
-                } else {
-                    this.playAudio();
-                }
-            } else {
-                this.currentPlaylistItem = index;
-                this.$nextTick(()=> {
-                    this.playAudio();
-                });
-            }
-        },
-        prevAudio() {
-            this.stopAudio();
-            if (this.currentPlaylistItem !== 0) {
-                this.currentPlaylistItem = this.currentPlaylistItem - 1;
-            } else {
-                this.currentPlaylistItem = this.files.length - 1;
-            }
-            this.$nextTick(()=> {
-                this.playAudio();
-            });
-        },
-        nextAudio() {
-            this.stopAudio();
-            if (this.currentPlaylistItem < this.files.length - 1) {
-                this.currentPlaylistItem = this.currentPlaylistItem + 1;
-                this.$nextTick(()=> {
-                    this.playAudio();
-                });
-            }
-        },
-
-        async loadFile() {
-            const id = this.currentFileId;
-            await this.loadFileRef({ id });
-            const fileRef = this.fileRefById({ id });
-
-            if (fileRef) {
-                this.updateCurrentFile({
-                    id: fileRef.id,
-                    name: fileRef.attributes.name,
-                    download_url: this.urlHelper.getURL(
-                        'sendfile.php',
-                        { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
-                        true
-                    ),
-                    mime_type: fileRef.attributes['mime-type']
-                });
-            }
-        },
-        enableRecorder() {
-            if (!this.folderSelected || this.folderLoadError) {
-                return false;
-            }
-            let view = this;
-            navigator.mediaDevices.getUserMedia({ audio: true })
-                .then(function(stream) {
-                    view.recorder = new MediaRecorder(stream, {type: 'audio/webm; codecs:vp9' });
-                    view.userRecorderEnabled = true;
-                    view.recorder.ondataavailable = e => {
-                        view.chunks.push(e.data);
-                    };
-                })
-                .catch(() => {
-                    view.companionWarning({
-                        info: view.$gettext('Sie müssen ein Mikrofon freigeben, um eine Aufnahme starten zu können.')
-                    });
-                });
-        },
-        startRecording() {
-            let view = this;
-            this.chunks = [];
-            this.timer = 0;
-            this.recorder.start();
-            this.isRecording = true;
-            setTimeout(function(){ view.setTimer(); }, 1000);
-        },
-        stopRecording() {
-            this.isRecording = false;
-            this.newRecording = true;
-            this.recorder.stop();
-        },
-        setTimer() {
-            let view = this;
-            if (this.recorder.state === 'recording') {
-                this.timer++;
-                setTimeout(function(){ view.setTimer(); }, 1000);
-            }
-        },
-        async storeRecording() {
-            let view = this;
-            let user = this.usersById({id: this.userId});
-            let blob = new Blob(view.chunks, {type: 'audio/webm; codecs:vp9' });
-            let file = {
-                attributes: {
-                    name: (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '.webm'
-                },
-                relationships: {
-                    'terms-of-use': {
-                        data: {
-                            id: 'SELFMADE_NONPUB'
-                        }
-                    }
-                }
-            };
-            let fileObj = await this.createFile({
-                file: file,
-                filedata: blob,
-                folder: {id: this.currentFolderId}
-            });
-            if(fileObj && fileObj.type === 'file-refs') {
-                this.companionSuccess({
-                    info: this.$gettext('Die Aufnahme wurde erfolgreich im Dateibereich abgelegt.')
-                });
-            } else {
-                this.companionError({
-                    info: this.$gettext('Es ist ein Fehler aufgetreten! Die Aufnahme konnte nicht gespeichert werden.')
-                });
-            }
-            this.newRecording = false;
-            this.getFolderFiles();
-        },
-        resetRecorder() {
-            this.newRecording = false;
-            this.chunks = [];
-            this.timer = 0;
-            this.blob = null;
-        },
-    },
-    watch: {
-        currentFolderId(newState) {
-            if (newState === '') {
-                this.currentRecorderEnabled = false;
-            } else {
-                this.getFolderFiles();
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue b/resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue
deleted file mode 100644
index 2770387..0000000
--- a/resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue
+++ /dev/null
@@ -1,287 +0,0 @@
-<template>
-    <div class="cw-block cw-block-before-after">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <TwentyTwenty v-if="!isEmpty" :before="currentBeforeUrl" :after="currentAfterUrl" />
-                <courseware-companion-box
-                    v-if="isEmpty && editMode"
-                    :msgCompanion="$gettext('Bitte wählen Sie ein Vorher- und ein Nachher-Bild aus.')"
-                    mood="pointing"
-                />
-            </template>
-            <template v-if="canEdit" #edit>
-                <courseware-tabs>
-                    <courseware-tab
-                        :index="0"
-                        :name="$gettext('Vorher')"
-                        :selected="true"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Quelle') }}
-                                <select v-model="currentBeforeSource">
-                                    <option value="studip">{{ $gettext('Dateibereich') }}</option>
-                                    <option value="web">{{ $gettext('Web-Adresse') }}</option>
-                                </select>
-                            </label>
-                            <label v-if="currentBeforeSource === 'web'">
-                                {{ $gettext('URL') }}
-                                <input type="text" v-model="currentBeforeWebUrl" />
-                            </label>
-                            <label v-if="currentBeforeSource === 'studip'">
-                                {{ $gettext('Bilddatei') }}
-                                <courseware-file-chooser
-                                    v-model="currentBeforeFileId"
-                                    :isImage="true"
-                                    @selectFile="updateCurrentBeforeFile"
-                                />
-                            </label>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="1"
-                        :name="$gettext('Nachher')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Quelle') }}
-                                <select v-model="currentAfterSource">
-                                    <option value="studip">{{ $gettext('Dateibereich') }}</option>
-                                    <option value="web">{{ $gettext('Web-Adresse') }}</option>
-                                </select>
-                            </label>
-                            <label v-if="currentAfterSource === 'web'">
-                                {{ $gettext('URL') }}
-                                <input type="text" v-model="currentAfterWebUrl" />
-                            </label>
-                            <label v-if="currentAfterSource === 'studip'">
-                                {{ $gettext('Bilddatei') }}
-                                <courseware-file-chooser
-                                    v-model="currentAfterFileId"
-                                    :isImage="true"
-                                    @selectFile="updateCurrentAfterFile"
-                                />
-                            </label>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Bildvergleich-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFileChooser from './CoursewareFileChooser.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import { blockMixin } from './block-mixin.js';
-import TwentyTwenty from 'vue-twentytwenty';
-import 'vue-twentytwenty/dist/vue-twentytwenty.css';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-before-after-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareCompanionBox,
-        CoursewareDefaultBlock,
-        CoursewareFileChooser,
-        CoursewareTabs,
-        CoursewareTab,
-        TwentyTwenty,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentBeforeSource: '',
-            currentBeforeFileId: '',
-            currentBeforeFile: {},
-            currentBeforeWebUrl: '',
-            currentAfterSource: '',
-            currentAfterFileId: '',
-            currentAfterFile: {},
-            currentAfterWebUrl: '',
-            afterFile: null,
-            beforeFile: null
-        };
-    },
-    computed: {
-        ...mapGetters({
-            viewMode: 'viewMode'
-        }),
-        beforeSource() {
-            return this.block?.attributes?.payload?.before_source;
-        },
-        beforeFileId() {
-            return this.block?.attributes?.payload?.before_file_id;
-        },
-        beforeWebUrl() {
-            return this.block?.attributes?.payload?.before_web_url;
-        },
-        afterSource() {
-            return this.block?.attributes?.payload?.after_source;
-        },
-        afterFileId() {
-            return this.block?.attributes?.payload?.after_file_id;
-        },
-        afterWebUrl() {
-            return this.block?.attributes?.payload?.after_web_url;
-        },
-        currentBeforeUrl() {
-            if (this.currentBeforeSource === 'studip' && this.currentBeforeFile?.meta) {
-                return this.currentBeforeFile.meta['download-url'];
-            } else if (this.currentBeforeSource === 'web') {
-                return this.currentBeforeWebUrl;
-            } else {
-                return '';
-            }
-        },
-        currentAfterUrl() {
-            if (this.currentAfterSource === 'studip' && this.currentAfterFile?.meta) {
-                return this.currentAfterFile.meta['download-url'];
-            } else if (this.currentAfterSource === 'web') {
-                return this.currentAfterWebUrl;
-            } else {
-                return '';
-            }
-        },
-        isEmpty() {
-            return this.currentBeforeUrl === '' || this.currentAfterUrl === '';
-        },
-        editMode() {
-            return this.viewMode === 'edit';
-        }
-    },
-    mounted() {
-        this.loadFileRefs(this.block.id).then((response) => {
-            for (let i = 0; i < response.length; i++) {
-                if (response[i].id === this.beforeFileId) {
-                    this.beforeFile = response[i];
-                }
-
-                if (response[i].id === this.afterFileId) {
-                    this.afterFile = response[i];
-                }
-            }
-
-            this.currentBeforeFile = this.beforeFile;
-            this.currentAfterFile  = this.afterFile;
-        });
-
-        this.loadImages();
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            loadFileRefs: 'loadFileRefs',
-            companionWarning: 'companionWarning',
-        }),
-        loadImages() {
-            this.loadFileRefs(this.block.id).then((response) => {
-                for (let i = 0; i < response.length; i++) {
-                    if (response[i].id === this.beforeFileId) {
-                        this.beforeFile = response[i];
-                    }
-
-                    if (response[i].id === this.afterFileId) {
-                        this.afterFile = response[i];
-                    }
-                }
-
-                this.currentBeforeFile = this.beforeFile;
-                this.currentAfterFile  = this.afterFile;
-            });
-        },
-
-        initCurrentData() {
-            this.currentBeforeSource = this.beforeSource;
-            this.currentBeforeFileId = this.beforeFileId;
-            this.currentBeforeWebUrl = this.beforeWebUrl;
-            this.currentAfterSource = this.afterSource;
-            this.currentAfterFileId = this.afterFileId;
-            this.currentAfterWebUrl = this.afterWebUrl;
-        },
-        updateCurrentBeforeFile(file) {
-            this.currentBeforeFile = file;
-            this.currentBeforeFileId = file.id;
-        },
-        updateCurrentAfterFile(file) {
-            this.currentAfterFile = file;
-            this.currentAfterFileId = file.id;
-        },
-        storeBlock() {
-            let cmpInfo = false;
-            let cmpInfoBefore = this.$gettext('Bitte wählen Sie ein Vorherbild aus.');
-            let cmpInfoAfter = this.$gettext('Bitte wählen Sie ein Nachherbild aus.');
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.before_source = this.currentBeforeSource;
-            attributes.payload.after_source = this.currentAfterSource;
-            if (this.currentAfterSource === 'studip') {
-                if (this.currentAfterFile === null) {
-                    cmpInfo = cmpInfoAfter;
-                } else {
-                    attributes.payload.after_file_id = this.currentAfterFile.id;
-                    attributes.payload.after_web_url = '';
-                }
-            } else if (this.currentAfterSource === 'web') {
-                if (this.currentAfterWebUrl === '') {
-                    cmpInfo = cmpInfoAfter;
-                } else {
-                    attributes.payload.after_file_id = '';
-                    attributes.payload.after_web_url = this.currentAfterWebUrl;
-                }
-
-            } else {
-                cmpInfo = cmpInfoAfter;
-            }
-            if (this.currentBeforeSource === 'studip') {
-                if (this.currentBeforeFile === null) {
-                    cmpInfo = cmpInfoBefore;
-                } else {
-                    attributes.payload.before_file_id = this.currentBeforeFile.id;
-                    attributes.payload.before_web_url = '';
-                }
-            } else if (this.currentBeforeSource === 'web') {
-                if (this.currentBeforeWebUrl === '') {
-                    cmpInfo = cmpInfoBefore;
-                } else {
-                    attributes.payload.before_file_id = '';
-                    attributes.payload.before_web_url = this.currentBeforeWebUrl;
-                }
-            } else {
-                cmpInfo = cmpInfoBefore;
-            }
-
-            if (cmpInfo) {
-                this.companionWarning({
-                    info: cmpInfo
-                });
-                return false;
-            } else {
-                this.updateBlock({
-                    attributes: attributes,
-                    blockId: this.block.id,
-                    containerId: this.block.relationships.container.data.id,
-                });
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBiographyAchievementsBlock.vue b/resources/vue/components/courseware/CoursewareBiographyAchievementsBlock.vue
deleted file mode 100644
index 2054040..0000000
--- a/resources/vue/components/courseware/CoursewareBiographyAchievementsBlock.vue
+++ /dev/null
@@ -1,200 +0,0 @@
-<template>
-    <div class="cw-block cw-block-biography cw-block-biography-achievements">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @storeEdit="storeBlock"
-            @showEdit="setShowEdit"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div class="cw-block-biography-content" >
-                    <div class="cw-block-biography-type" :class="'cw-block-biography-achievements-type-' + currentData.type">
-                        <h2>{{ typeName }}</h2>
-                    </div>
-                    <div class="cw-block-biography-details">
-                        <h3>
-                            {{ $gettext('Titel') }}: {{currentData.title}}
-                        </h3>
-                        <h4>
-                            <span v-show="currentData.type !== 'membership'">{{ $gettext('Datum') }}:</span>
-                            <span v-show="currentData.type === 'membership'">{{ $gettext('Startdatum') }}:</span>
-                            {{ getReadableDate(currentData.date) }}
-                        </h4>
-                        <h4 v-show="hasEndDate">
-                            {{ $gettext('Enddatum') }}: {{ getReadableDate(currentData.end_date)}}
-                        </h4>
-                        <h4 v-show="hasParticipation">
-                            {{ $gettext('Beteiligung') }}: {{ currentData.role }}
-                        </h4>
-                        <div>
-                            <h4>{{ $gettext('Beschreibung') }}:</h4>
-                            <div class="formatted-content ck-content" v-html="currentData.description"></div>
-                        </div>
-                    </div>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Type') }}
-                        <select v-model="currentData.type">
-                            <option value="certificate">{{ $gettext('Zertifikat') }}</option>
-                            <option value="accreditation">{{ $gettext('Akkreditierung') }}</option>
-                            <option value="award">{{ $gettext('Auszeichnung') }}</option>
-                            <option value="book">{{ $gettext('Buch') }}</option>
-                            <option value="publication">{{ $gettext('Veröffentlichung') }}</option>
-                            <option value="membership">{{ $gettext('Mitgliedschaft') }}</option>
-                        </select>
-                    </label>
-                    <label>
-                        {{ $gettext('Titel') }}
-                        <input type="text" v-model="currentData.title">
-                    </label>
-                    <label>
-                        <template v-if="!hasEndDate">{{ $gettext('Datum') }}</template>
-                        <template v-else>{{ $gettext('Startdatum') }}</template>
-                        <input type="date" v-model="currentData.date" />
-                    </label>
-                    <label v-show="hasEndDate">
-                        {{ $gettext('Enddatum') }}
-                        <input type="date" v-model="currentData.end_date" />
-                    </label>
-                    <label v-show="hasParticipation">
-                        {{ $gettext('Beteiligung') }}
-                        <input type="text" v-model="currentData.role">
-                    </label>
-                    <div class="label-text">
-                        {{ $gettext('Beschreibung') }}
-                    </div>
-                    <studip-wysiwyg v-model="currentData.description"></studip-wysiwyg>
-                </form>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Erfolge-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { mapActions } from 'vuex';
-import { blockMixin } from './block-mixin.js';
-import StudipWysiwyg from '../StudipWysiwyg.vue';
-
-export default {
-    name: 'courseware-biography-achievements-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        StudipWysiwyg,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentData: {},
-            showEdit: false,
-        }
-    },
-    computed: {
-        payload() {
-            return this.block?.attributes?.payload;
-        },
-        typeName() {
-            switch (this.currentData.type) {
-                case 'certificate':
-                    return this.$gettext('Zertifikat');
-                case 'accreditation':
-                    return this.$gettext('Akkreditierung');
-                case 'award':
-                    return this.$gettext('Auszeichnung');
-                case 'book':
-                    return this.$gettext('Buch');
-                case 'publication':
-                    return this.$gettext('Veröffentlichung');
-                case 'membership':
-                    return this.$gettext('Mitgliedschaft');
-                default:
-                    return '';
-            }
-        },
-        achievementClass() {
-            switch (this.currentData.type) {
-                case 'certificate':
-                case 'accreditation':
-                case 'award':
-                    return 'certificate';
-                case 'book':
-                case 'publication':
-                    return 'publication';
-                case 'membership':
-                    return 'membership';
-                default:
-                    return '';
-            }
-        },
-        hasParticipation() {
-            return ['book', 'publication'].includes(this.currentData.type);
-        },
-        hasEndDate() {
-            return this.currentData.type === 'membership';
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    updated() {
-        this.updateCanvas();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            if (this.payload) {
-                this.currentData = this.payload;
-                this.currentData.date = this.getInputDate(this.currentData.date);
-                this.currentData.end_date = this.getInputDate(this.currentData.end_date);
-            }
-        },
-        setShowEdit(state) {
-            this.showEdit = state;
-        },
-        getInputDate(inputDate) {
-            let date = new Date(inputDate);
-            return date.getFullYear() + '-' +
-                ('0' + (date.getMonth() + 1)).slice(-2) + '-' +
-                ('0' + date.getDate()).slice(-2);
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {
-                type: this.currentData.type,
-                title: this.currentData.title,
-                date: new Date(this.currentData.date).getTime(),
-                end_date: new Date(this.currentData.end_date).getTime(),
-                role: this.currentData.role,
-                description: this.currentData.description
-            };
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-    watch: {
-        payload() {
-            if (!this.showEdit) {
-                this.initCurrentData();
-            }
-        },
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBiographyCareerBlock.vue b/resources/vue/components/courseware/CoursewareBiographyCareerBlock.vue
deleted file mode 100644
index 6fd8550..0000000
--- a/resources/vue/components/courseware/CoursewareBiographyCareerBlock.vue
+++ /dev/null
@@ -1,262 +0,0 @@
-<template>
-    <div class="cw-block cw-block-biography-career">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @closeEdit="initCurrentData"
-            @showEdit="setShowEdit"
-            @storeEdit="storeBlock"
-        >
-            <template #content>
-                <ol class="cw-timeline">
-                    <li 
-                        v-for="(item, index) in sortedItems"
-                        :key="index"
-                        class="cw-timeline-item"
-                    >
-                        <div class="cw-timeline-item-icon cw-timeline-item-icon-color-studip-blue">
-                            <studip-icon v-if="item.type === 'school'" shape="doctoral-cap" role="clickable" size="32"/>
-                            <studip-icon v-if="item.type === 'experience'" shape="tools" role="clickable" size="32"/>
-                        </div>
-                        <div
-                            class="cw-timeline-item-content cw-timeline-item-content-color-studip-blue"
-                            :class="[index % 2 === 0 ? 'left' : 'right',]"
-                        >
-                            <h3>{{ item.date ? getReadableDate(item.date) : ''}}{{ item.enddate ? ' - ' + getReadableDate(item.enddate) : '' }}</h3>
-                            <article>
-                                <header>{{ getItemTypeName(item.type) }}</header>
-                                <div v-if="item.type === 'school'">
-                                    <p>{{ $gettext('Bezeichnung der Qualifikation') }}: {{ item.qualification }}</p>
-                                    <p>{{ $gettext('Hauptfächer / Schwerpunkt') }}: {{ item.focus }}</p>
-                                    <p>{{ $gettext('berufliche Fähigkeiten') }}: {{ item.skills }}</p>
-                                </div>
-                                <div v-if="item.type === 'experience'">
-                                    <p>{{ $gettext('Name des Arbeitgebers') }}: {{ item.employer }}</p>
-                                    <p>{{ $gettext('Beruf / Funktion') }}: {{ item.job }}</p>
-                                </div>
-                            </article>
-                        </div>
-                    </li>
-                </ol>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Zeitliche Sortierung') }}
-                        <select v-model="currentSort">
-                            <option value="none">{{ $gettext('Keine') }}</option>
-                            <option value="asc">{{ $gettext('Aufsteigend') }}</option>
-                            <option value="desc">{{ $gettext('Absteigend') }}</option>
-                        </select>
-                    </label>
-                </form>
-                <button class="button add" @click="addItem">{{ $gettext('Ereignis hinzufügen') }}</button>
-                <courseware-tabs
-                    v-if="currentItems.length > 0"
-                    :setSelected="setItemTab"
-                    @selectTab="setItemTab = (parseInt($event.name.replace($gettext('Ereignis') +  ' ', '')) - 1)"
-                >
-                    <courseware-tab
-                        v-for="(item, index) in currentItems"
-                        :key="index"
-                        :index="index"
-                        :name="$gettext('Ereignis') +  ' ' + (index + 1).toString()"
-                        :selected="index === 0"
-                        canBeEmpty
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label class="col-1">
-                                {{ $gettext('Startdatum') }}
-                                <input type="date" v-model="item.date" required />
-                            </label>
-                            <label class="col-1">
-                                {{ $gettext('Enddatum') }}
-                                <input type="date" v-model="item.enddate" />
-                            </label>
-                            <label>
-                                {{ $gettext('Art') }}
-                                <select v-model="item.type">
-                                    <option value="school">{{ $gettext('Schul- und Berufsbildung') }}</option>
-                                    <option value="experience">{{ $gettext('Berufserfahrung') }}</option>
-                                </select>
-                            </label>
-                            <div v-show="item.type === 'school'">
-                                <label>
-                                    {{ $gettext('Bezeichnung der Qualifikation') }}
-                                    <input type="text" v-model="item.qualification" />
-                                </label>
-                                <label>
-                                    {{ $gettext('Hauptfächer / Schwerpunkt') }}
-                                    <input type="text" v-model="item.focus" />
-                                </label>
-                                <label>
-                                    {{ $gettext('berufliche Fähigkeiten') }}
-                                    <input type="text" v-model="item.skills" />
-                                </label>
-                            </div>
-                            <div v-show="item.type === 'experience'">
-                                <label>
-                                    {{ $gettext('Name des Arbeitgebers') }}
-                                    <input type="text" v-model="item.employer" />
-                                </label>
-                                <label>
-                                    {{ $gettext('Beruf / Funktion') }}
-                                    <input type="text" v-model="item.job" />
-                                </label>
-                            </div>
-                            <label>
-                                {{ $gettext('Beschreibung') }}
-                                <textarea v-model="item.description" />
-                            </label>
-                            <label v-if="currentItems.length > 1">
-                                <button class="button trash" @click="removeItem(index)">
-                                    {{ $gettext('Ereignis entfernen') }}
-                                </button>
-                            </label>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Karriere-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import { mapActions } from 'vuex';
-import { blockMixin } from './block-mixin.js';
-import StudipIcon from '../StudipIcon.vue';
-
-export default {
-    name: 'courseware-biography-career-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareTabs,
-        CoursewareTab,
-        StudipIcon,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            showEdit: false,
-            setItemTab: 0,
-            currentItems: [],
-            currentSort: '',
-        }
-    },
-    computed: {
-        items() {
-            return this.block?.attributes?.payload?.items;
-        },
-        sort() {
-            return this.block?.attributes?.payload?.sort;
-        },
-        sortedItems() {
-            if (this.currentSort === 'none') {
-                return this.currentItems;
-            }
-            let view = this;
-            let items = _.cloneDeep(this.currentItems);
-            return items.sort((a, b) => {
-                let dateA = null;
-                let dateB = null;
-
-                if (a.time) {
-                    dateA = new Date(a.date + 'T' + a.time);
-                } else {
-                    dateA = new Date(a.date);
-                }
-                if (b.time) {
-                    dateB = new Date(b.date + 'T' + b.time);
-                } else {
-                    dateB = new Date(b.date);
-                }
-                if (view.currentSort === 'asc') {
-                    return dateA > dateB ? 1 : dateA < dateB ? -1 : 0;
-                }
-                if (view.currentSort === 'desc') {
-                    return dateA < dateB ? 1 : dateA > dateB ? -1 : 0;
-                }
-            });
-        }
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        setShowEdit(state) {
-            this.showEdit = state;
-        },
-        initCurrentData() {
-            this.currentItems = this.items;
-            this.currentSort = this.sort;
-            this.setItemTab = 0;
-        },
-        addItem() {
-            this.currentItems.push({
-                title: '',
-                description: '',
-                date: '',
-                enddate: '',
-                type: 'school',
-                qualification: '',
-                focus: '',
-                skills: '',
-                employer: '',
-                job: '',
-            });
-            this.$nextTick(() => { this.setItemTab = this.currentItems.length - 1; });
-        },
-        removeItem(itemIndex){
-            this.currentItems = this.currentItems.filter((val, index) => {
-                return !(index === itemIndex);
-            });
-            this.$nextTick(() => { this.setItemTab = 0; });
-        },
-        getItemTypeName(type) {
-            switch (type) {
-                case 'school':
-                    return this.$gettext('Schul- und Berufsbildung');
-                case 'experience':
-                    return this.$gettext('Berufserfahrung');
-            }
-
-            return '';
-        },
-        storeBlock() {
-            let attributes = {
-                payload: {
-                    sort: this.currentSort,
-                    items: this.currentItems
-                }
-            };
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-    watch: {
-        items() {
-            if (!this.showEdit) {
-                this.initCurrentData();
-            }
-        },
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBiographyGoalsBlock.vue b/resources/vue/components/courseware/CoursewareBiographyGoalsBlock.vue
deleted file mode 100644
index 857672f..0000000
--- a/resources/vue/components/courseware/CoursewareBiographyGoalsBlock.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<template>
-    <div class="cw-block cw-block-biography cw-block-biography-goals">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @closeEdit="initCurrentData"
-            @showEdit="setShowEdit"
-            @storeEdit="storeBlock"
-        >
-            <template #content>
-                <div class="cw-block-biography-content" >
-                    <div class="cw-block-biography-type" :class="'cw-block-biography-goals-type-' + currentData.type">
-                        <h2>{{ goalTypeName }}</h2>
-                    </div>
-                    <div class="cw-block-biography-details formatted-content ck-content" v-html="currentData.description">
-                    </div>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default">
-                    <label for="type">
-                        {{ $gettext('Ziel') }}
-                        <select name="type" class="type" v-model="currentData.type">
-                            <option value="personal">{{ $gettext('Persönliches Ziel') }}</option>
-                            <option value="school">{{ $gettext('Schulisches Ziel') }}</option>
-                            <option value="academic">{{ $gettext('Akademisches Ziel') }}</option>
-                            <option value="professional">{{ $gettext('Berufliches Ziel') }}</option>
-                        </select>
-                    </label>
-                    <div class="label-text">
-                       {{ $gettext('Beschreibung') }}
-                    </div>
-                    <studip-wysiwyg v-model="currentData.description"></studip-wysiwyg>
-                </form>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Ziele-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { mapActions } from 'vuex';
-import { blockMixin } from './block-mixin.js';
-import StudipWysiwyg from '../StudipWysiwyg.vue';
-
-export default {
-    name: 'courseware-biography-goals-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        StudipWysiwyg,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            showEdit: false,
-            currentData: {
-                type: '',
-                description: ''
-            },
-        }
-    },
-    computed: {
-        type() {
-            return this.block?.attributes?.payload?.type;
-        },
-        description() {
-            return this.block?.attributes?.payload?.description;
-        },
-        goalTypeName() {
-            switch (this.currentData.type) {
-                case 'personal':
-                    return this.$gettext('Persönliches Ziel');
-                case 'school':
-                    return this.$gettext('Schulisches Ziel');
-                case 'academic':
-                    return this.$gettext('Akademisches Ziel');
-                case 'professional':
-                    return this.$gettext('Berufliches Ziel');
-            }
-
-            throw new Error('Undefined data type ' + this.currentData.type);
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            this.currentData = {
-                type: this.type,
-                description: this.description
-            };
-        },
-        setShowEdit(state) {
-            this.showEdit = state;
-        },
-        storeBlock() {
-            let attributes = {
-                payload: {
-                    type: this.currentData.type,
-                    description: this.currentData.description
-                }
-            };
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-    watch: {
-        type() {
-            if (!this.showEdit) {
-                this.currentData.type = this.type;
-            }
-        },
-        description() {
-            if (!this.showEdit) {
-                this.currentData.description = this.description;
-            }
-        },
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBiographyPersonalInformationBlock.vue b/resources/vue/components/courseware/CoursewareBiographyPersonalInformationBlock.vue
deleted file mode 100644
index 28e5ffc..0000000
--- a/resources/vue/components/courseware/CoursewareBiographyPersonalInformationBlock.vue
+++ /dev/null
@@ -1,185 +0,0 @@
-<template>
-    <div class="cw-block cw-block-biography cw-block-biography-personal-information">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @closeEdit="initCurrentData"
-            @showEdit="setShowEdit"
-            @storeEdit="storeBlock"
-        >
-            <template #content>
-                <div class="cw-block-biography-content" >
-                    <div class="cw-block-biography-type cw-block-biography-personal-information-type">
-                        <h2>{{currentData.name}}</h2>
-                    </div>
-                    <div class="cw-block-biography-details cw-block-biography-personal-information-details">
-                        <span>{{ $gettext('Geburtsort') }}:</span>
-                        <span>{{currentData.birthplace}}</span>
-
-                        <span>{{ $gettext('Geburtsdatum') }}:</span>
-                        <span>{{ getReadableDate(currentData.birthday) }}</span>
-
-                        <span>{{ $gettext('Geschlecht') }}:</span>
-                        <span>{{displayGenderText(currentData.gender)}}</span>
-
-                        <span>{{ $gettext('Familienstand') }}:</span>
-                        <span>{{ displayStatusText(currentData.status) }}</span>
-                    </div>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Name') }}
-                        <input type="text" v-model="currentData.name">
-                    </label>
-                    <label>
-                        {{ $gettext('Geburtsort') }}
-                        <input type="text" v-model="currentData.birthplace">
-                    </label>
-                    <label>
-                        {{ $gettext('Geburtsdatum') }}
-                        <input type="date" v-model="currentData.birthday" />
-                    </label>
-                    <label>
-                        {{ $gettext('Geschlecht') }}
-                        <select v-model="currentData.gender">
-                            <option value="none">{{ displayGenderText('none') }}</option>
-                            <option value="male">{{ displayGenderText('male') }}</option>
-                            <option value="female">{{ displayGenderText('female') }}</option>
-                            <option value="diverse">{{ displayGenderText('diverse') }}</option>
-                        </select>
-                    </label>
-                    <label>
-                        {{ $gettext('Familienstand') }}
-                        <select v-model="currentData.status">
-                            <option value="none">{{ displayStatusText('none') }}</option>
-                            <option value="single">{{ displayStatusText('single') }}</option>
-                            <option value="married">{{ displayStatusText('married') }}</option>
-                            <option value="widowed">{{ displayStatusText('widowed') }}</option>
-                            <option value="divorced">{{ displayStatusText('divorced') }}</option>
-                            <option value="registered-civil-partnership">{{ displayStatusText('registered-civil-partnership') }}</option>
-                            <option value="annulled-civil-partnership">{{ displayStatusText('annulled-civil-partnership') }}</option>
-                        </select>
-                    </label>
-                </form>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Persönlichen-Informationen-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { mapActions } from 'vuex';
-import { blockMixin } from './block-mixin.js';
-
-export default {
-    name: 'courseware-biography-personal-information-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            showEdit: false,
-            currentData: {},
-        }
-    },
-    computed: {
-        payload() {
-            return this.block?.attributes?.payload;
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        changeDate(date) {
-            this.currentData.birthday = date;
-        },
-        initCurrentData() {
-            if (this.payload) {
-                this.currentData = this.payload;
-                this.currentData.birthday = this.getInputDate(this.currentData.birthday);
-            }
-        },
-        getInputDate(inputDate) {
-            let date = new Date(inputDate);
-            return date.getFullYear() + '-' +
-                ('0' + (date.getMonth() + 1)).slice(-2) + '-' +
-                ('0' + date.getDate()).slice(-2);
-        },
-        displayGenderText(gender) {
-            switch (gender) {
-                case 'none':
-                    return this.$gettext('keine Angabe');
-                case 'male':
-                    return this.$gettext('männlich');
-                case 'female':
-                    return this.$gettext('Weiblich');
-                case 'diverse':
-                    return this.$gettext('divers');
-                default:
-                    return '';
-            }
-        },
-        displayStatusText(status) {
-            switch (status) {
-                case 'none':
-                    return this.$gettext('keine Angabe');
-                case 'single':
-                    return this.$gettext('ledig');
-                case 'married':
-                    return this.$gettext('verheiratet');
-                case 'widowed':
-                    return this.$gettext('verwitwet');
-                case 'divorced':
-                    return this.$gettext('geschieden');
-                case 'registered-civil-partnership':
-                    return this.$gettext('eingetragene Lebenspartnerschaft');
-                case 'annulled-civil-partnership':
-                    return this.$gettext('aufgehobene Lebenspartnerschaft');
-                default:
-                    return '';
-            }
-        },
-        setShowEdit(state) {
-            this.showEdit = state;
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {
-                name: this.currentData.name,
-                birthplace: this.currentData.birthplace,
-                birthday: new Date(this.currentData.birthday).getTime(),
-                gender: this.currentData.gender,
-                status: this.currentData.status
-            };
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-    watch: {
-        payload() {
-            if (!this.showEdit) {
-                this.initCurrentData();
-            }
-        },
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockActions.vue b/resources/vue/components/courseware/CoursewareBlockActions.vue
deleted file mode 100644
index 2b62527..0000000
--- a/resources/vue/components/courseware/CoursewareBlockActions.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<template>
-    <div class="cw-block-actions">
-        <studip-action-menu
-            :items="menuItems"
-            :context="block.attributes.title"
-            @editBlock="editBlock"
-            @setVisibility="setVisibility"
-            @showInfo="showInfo"
-            @deleteBlock="deleteBlock"
-            @removeLock="removeLock"
-            @copyToClipboard="copyToClipboard"
-        />
-    </div>
-</template>
-
-<script>
-import StudipActionMenu from './../StudipActionMenu.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-block-actions',
-    components: {
-        StudipActionMenu,
-    },
-    props: {
-        canEdit: Boolean,
-        deleteOnly: {
-            type: Boolean,
-            default: false
-        },
-        block: Object,
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            userIsTeacher: 'userIsTeacher',
-        }),
-        blocked() {
-            return this.block?.relationships?.['edit-blocker']?.data !== null;
-        },
-        blockerId() {
-            return this.blocked ? this.block?.relationships?.['edit-blocker']?.data?.id : null;
-        },
-        blockedByThisUser() {
-            return this.blocked && this.userId === this.blockerId;
-        },
-        blockedByAnotherUser() {
-            return this.blocked && this.userId !== this.blockerId;
-        },
-        menuItems() {
-            let menuItems = [];
-            if (this.canEdit) {
-                if (!this.deleteOnly) {
-                    if (!this.blocked) {
-                        menuItems.push({ id: 1, label: this.$gettext('Block bearbeiten'), icon: 'edit', emit: 'editBlock' });
-                        menuItems.push({
-                            id: 3,
-                            label: this.block.attributes.visible
-                                ? this.$gettext('unsichtbar setzen')
-                                : this.$gettext('sichtbar setzen'),
-                            icon: this.block.attributes.visible ? 'visibility-visible' : 'visibility-invisible', // do we change the icons ?
-                            emit: 'setVisibility',
-                        });
-                    }
-                    if (this.blocked && this.blockedByAnotherUser && this.userIsTeacher) {
-                        menuItems.push({
-                            id: 8,
-                            label: this.$gettext('Sperre aufheben'),
-                            icon: 'lock-unlocked',
-                            emit: 'removeLock',
-                        });
-                    }
-                    if (!this.blocked || this.blockedByThisUser) {
-                        menuItems.push({
-                            id: 9,
-                            label: this.$gettext('Block löschen'), 
-                            icon: 'trash',
-                            emit: 'deleteBlock' 
-                        });
-                    }
-                    menuItems.push({
-                        id: 2,
-                        label: this.$gettext('Block merken'),
-                        icon: 'clipboard',
-                        emit: 'copyToClipboard'
-                    });
-                    menuItems.push({
-                        id: 7,
-                        label: this.$gettext('Informationen zum Block'),
-                        icon: 'info',
-                        emit: 'showInfo',
-                    });
-                }
-            }
-            menuItems.sort((a, b) => {
-                return a.id > b.id ? 1 : b.id > a.id ? -1 : 0;
-            });
-
-            return menuItems;
-        }
-
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-        }),
-        menuAction(action) {
-            this[action]();
-        },
-        editBlock() {
-            this.$emit('editBlock');
-        },
-        showInfo() {
-            this.$emit('showInfo');
-        },
-        showExportOptions() {
-            this.$emit('showExportOptions');
-        },
-        async setVisibility() {
-            if (!this.blocked) {
-                await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
-            } else {
-                if (this.blockerId !== this.userId) {
-                    return false;
-                }
-            }
-            let attributes = {};
-            attributes.visible = !this.block.attributes.visible;
-
-            await this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-
-            await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' });
-        },
-        deleteBlock() {
-            this.$emit('deleteBlock');
-        },
-        removeLock() {
-            this.$emit('removeLock');
-        },
-        copyToClipboard() {
-            this.$emit('copyToClipboard');
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockAdderArea.vue b/resources/vue/components/courseware/CoursewareBlockAdderArea.vue
deleted file mode 100644
index fc9b41f..0000000
--- a/resources/vue/components/courseware/CoursewareBlockAdderArea.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<template>
-    <button
-        class="cw-block-adder-area"
-        :class="{ 'cw-block-adder-active': adderActive }"
-        :aria-pressed="adderActive"
-        @click="selectBlockAdder"
-    >
-        <studip-icon v-show="!adderActive" shape="add" />
-        <studip-icon v-show="adderActive" shape="add" role="info_alt"/>
-        <span v-show="!adderActive"><translate>Block zu diesem Abschnitt hinzufügen</translate></span>
-        <span v-show="adderActive"><translate>Abschnitt aktiv - Blöcke werden hier eingefügt</translate></span>
-    </button>
-</template>
-
-<script>
-import StudipIcon from '../StudipIcon.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-  components: { StudipIcon },
-    name: 'courseware-block-adder-area',
-    props: {
-        container: Object,
-        section: Number,
-    },
-    data() {
-        return {
-            adderActive: false,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            adderStorage: 'blockAdder',
-        }),
-        adderDisable() {
-            return Object.keys(this.adderStorage).length !== 0 && !this.adderActive;
-        },
-    },
-    methods: {
-        ...mapActions({
-            coursewareBlockAdder: 'coursewareBlockAdder',
-            coursewareSelectedToolbarItem: 'coursewareSelectedToolbarItem',
-            coursewareShowToolbar: 'coursewareShowToolbar'
-        }),
-        selectBlockAdder() {
-            if (this.adderActive) {
-                this.adderActive = false;
-                this.coursewareBlockAdder({});
-            } else {
-                this.adderActive = true;
-                this.coursewareBlockAdder({ container: this.container, section: this.section });
-                this.coursewareSelectedToolbarItem('blockadder');
-                this.coursewareShowToolbar(true);
-            }
-        },
-    },
-    watch: {
-        adderStorage(newValue, oldValue) {
-            if (Object.keys(newValue).length === 0) {
-                this.adderActive = false;
-                this.$emit('updateContainerContent', oldValue);
-            } else {
-                if (newValue.container && newValue.container.id === this.container.id && newValue.section === this.section) {
-                    this.adderActive = true;
-                } else {
-                    this.adderActive = false;
-                }
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockComments.vue b/resources/vue/components/courseware/CoursewareBlockComments.vue
deleted file mode 100644
index 6ef3322..0000000
--- a/resources/vue/components/courseware/CoursewareBlockComments.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<template>
-    <section class="cw-block-comments" :class="[emptyComments ? 'cw-block-comments-empty' : '']">
-        <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
-        <div class="cw-block-features-content">
-            <div class="cw-block-comments-items" v-show="!emptyComments" ref="commentsRef">
-                <courseware-talk-bubble
-                    v-for="comment in comments"
-                    :key="comment.id"
-                    :payload="buildPayload(comment)"
-                />
-            </div>
-            <div class="cw-block-comment-create">
-                <textarea v-model="createComment" :placeholder="placeHolder" spellcheck="true"></textarea>
-                <button class="button" @click="postComment"><translate>Senden</translate></button>
-            </div>
-        </div>
-    </section>
-</template>
-
-<script>
-import CoursewareTalkBubble from './CoursewareTalkBubble.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-block-comments',
-    components: {
-        CoursewareTalkBubble,
-    },
-    props: {
-        block: Object,
-    },
-    data() {
-        return {
-            createComment: '',
-            placeHolder: this.$gettext('Stellen Sie eine Frage oder kommentieren Sie...'),
-            srMessage: ''
-        };
-    },
-    computed: {
-        ...mapGetters({
-            relatedUser: 'users/related',
-            userId: 'userId',
-            getComments: 'courseware-block-comments/related',
-        }),
-        comments() {
-            const parent = {
-                type: this.block.type,
-                id: this.block.id,
-            };
-
-            return this.getComments({ parent, relationship: 'comments' });
-        },
-        emptyComments() {
-            if (this.comments === null || this.comments.length === 0) {
-                return true;
-            }
-
-            return false;
-        }
-    },
-    methods: {
-        ...mapActions({
-            createComments: 'courseware-block-comments/create',
-            loadRelatedComments: 'courseware-block-comments/loadRelated'
-        }),
-        async loadComments() {
-            const parent = {
-                type: this.block.type,
-                id: this.block.id,
-            };
-            await this.loadRelatedComments({
-                parent,
-                relationship: 'comments',
-                options: {
-                    include: 'user',
-                },
-            });
-        },
-        async postComment() {
-            this.updateSrMessage(this.$gettext('Kommentar gesendet'));
-            const data = {
-                attributes: {
-                    comment: this.createComment
-                },
-                relationships: {
-                    block: {
-                        data: {
-                            id: this.block.id,
-                            type: this.block.type
-                        }
-                    }
-                }
-            };
-
-            await this.createComments(data);
-            this.loadComments();
-            this.createComment = '';
-        },
-        buildPayload(comment) {
-            const commenter = this.relatedUser({
-                parent: { id: comment.id, type: comment.type },
-                relationship: 'user',
-            });
-
-            const payload = {
-                id: comment.id,
-                own: comment.relationships.user.data.id === this.userId,
-                content: comment.attributes.comment,
-                chdate: comment.attributes.chdate,
-                mkdate: comment.attributes.mkdate,
-                user_id: commenter.id,
-                user_name: commenter.attributes['formatted-name'],
-                user_avatar: commenter.meta.avatar.small,
-            };
-
-            return payload;
-        },
-        updateSrMessage(message) {
-            this.srMessage = '';
-            this.srMessage = message;
-        }
-    },
-    mounted() {
-        this.loadComments();
-    },
-    updated() {
-        let ref = this.$refs["commentsRef"];
-        ref.scrollTop = ref.scrollHeight;
-    },
-    watch: {
-        comments() {
-            if (this.comments && this.comments.length > 0) {
-                this.$emit('hasComments');
-            }
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockDiscussion.vue b/resources/vue/components/courseware/CoursewareBlockDiscussion.vue
deleted file mode 100644
index e9ce9a3..0000000
--- a/resources/vue/components/courseware/CoursewareBlockDiscussion.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-    <div class="cw-block-discussion">
-        <courseware-collapsible-box
-            :title="text.comments"
-            :open="hasComments"
-        >
-            <courseware-block-comments
-            :block="block"
-            @hasComments="hasComments = true"
-            />
-        </courseware-collapsible-box>
-
-        <courseware-collapsible-box
-            v-if="canEdit || userIsTeacher"
-            :title="text.feedback"
-            :open="hasFeedback"
-        >
-            <courseware-block-feedback
-                :block="block"
-                :canEdit="canEdit"
-                @hasFeedback="hasFeedback = true"
-            />
-        </courseware-collapsible-box>
-    </div>
-</template>
-
-<script>
-import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
-import CoursewareBlockComments from './CoursewareBlockComments.vue';
-import CoursewareBlockFeedback from './CoursewareBlockFeedback.vue';
-import { mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-block-discussion',
-    components: {
-        CoursewareCollapsibleBox,
-        CoursewareBlockComments,
-        CoursewareBlockFeedback,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean
-    },
-    data() {
-        return {
-            hasComments: false,
-            hasFeedback: false,
-            text: {
-                comments: this.$gettext('Kommentare'),
-                feedback: this.$gettext('Feedback')
-            }
-        }
-    },
-    computed: {
-        ...mapGetters({
-            userIsTeacher: 'userIsTeacher',
-        }),
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockEdit.vue b/resources/vue/components/courseware/CoursewareBlockEdit.vue
deleted file mode 100644
index d1ed09f..0000000
--- a/resources/vue/components/courseware/CoursewareBlockEdit.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-    <section class="cw-block-edit">
-        <header v-if="preview">{{ $gettext('Bearbeiten') }}</header>
-        <div class="cw-block-features-content">
-            <div @click="deactivateToolbar(); exitHandler = true;">
-                <slot name="edit" />
-            </div>
-            <div class="cw-button-box">
-                <button class="button accept" @click="$emit('store'); exitHandler = false;">{{ $gettext('Speichern') }}</button>
-                <button class="button cancel" @click="$emit('close'); exitHandler = false;">{{ $gettext('Abbrechen') }}</button>
-            </div>
-        </div>
-    </section>
-</template>
-
-<script>
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-block-edit',
-    props: {
-        block: Object,
-        preview: Boolean
-    },
-    data() {
-        return {
-            originalBlock: Object,
-            exitHandler: false
-        };
-    },
-    beforeMount() {
-        this.originalBlock = this.block;
-    },
-    methods: {
-        ...mapActions({
-            coursewareBlockAdder: 'coursewareBlockAdder',
-            coursewareShowToolbar: 'coursewareShowToolbar'
-        }),
-        deactivateToolbar() {
-            this.coursewareBlockAdder({});
-            this.coursewareShowToolbar(false);
-        },
-    },
-    beforeDestroy() {
-        if (this.exitHandler) {
-            this.$emit('store');
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockExportOptions.vue b/resources/vue/components/courseware/CoursewareBlockExportOptions.vue
deleted file mode 100644
index 71f6adc..0000000
--- a/resources/vue/components/courseware/CoursewareBlockExportOptions.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-<template>
-    <section class="cw-block-export-options">
-        <header><translate>Export Optionen</translate></header>
-        <div class="cw-block-features-content">
-            <button class="button" @click="$emit('close')"><translate>Schließen</translate></button>
-        </div>
-    </section>
-</template>
-
-<script>
-export default {
-    name: 'courseware-block-export-options',
-    components: {},
-    props: {
-        block: Object,
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockFeedback.vue b/resources/vue/components/courseware/CoursewareBlockFeedback.vue
deleted file mode 100644
index a34ad8f..0000000
--- a/resources/vue/components/courseware/CoursewareBlockFeedback.vue
+++ /dev/null
@@ -1,141 +0,0 @@
-<template>
-    <section
-        v-if="canEdit || userIsTeacher"
-        class="cw-block-feedback"
-        :class="[emptyFeedback ? 'cw-block-feedback-empty' : '']"
-    >
-        <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
-        <div class="cw-block-features-content">
-            <div class="cw-block-feedback-items" v-show="!emptyFeedback" ref="feedbacks">
-                <courseware-talk-bubble
-                    v-for="feedback in feedback"
-                    :key="feedback.id"
-                    :payload="buildPayload(feedback)"
-                />
-            </div>
-            <courseware-companion-box
-                v-if="!userIsTeacher && feedback.length === 0"
-                :msgCompanion="$gettext('Es wurde noch kein Feedback abgegeben.')"
-                mood="pointing"
-            />
-            <div v-if="userIsTeacher" class="cw-block-feedback-create">
-                <textarea v-model="feedbackText" :placeholder="placeHolder" spellcheck="true"></textarea>
-                <button class="button" @click="postFeedback">{{ $gettext('Senden') }}</button>
-            </div>
-        </div>
-    </section>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareTalkBubble from './CoursewareTalkBubble.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-
-export default {
-    name: 'courseware-block-feedback',
-    components: {
-        CoursewareCompanionBox,
-        CoursewareTalkBubble,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-    },
-    data() {
-        return {
-            feedbackText: '',
-            placeHolder: this.$gettext('Schreiben Sie ein Feedback...'),
-            srMessage: ''
-        };
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            getRelatedFeedback: 'courseware-block-feedback/related',
-            getRelatedUser: 'users/related',
-            userIsTeacher: 'userIsTeacher',
-        }),
-        feedback() {
-            const { id, type } = this.block;
-
-            return this.getRelatedFeedback({ parent: { id, type }, relationship: 'feedback' });
-        },
-        emptyFeedback() {
-            if (this.feedback === null || this.feedback.length === 0) {
-                return true;
-            }
-
-            return false;
-        }
-    },
-    methods: {
-        ...mapActions({
-            createFeedback: 'courseware-block-feedback/create',
-            loadRelatedFeedback: 'courseware-block-feedback/loadRelated',
-        }),
-        buildPayload(feedback) {
-            const { id, type } = feedback;
-            const user = this.getRelatedUser({ parent: { id, type }, relationship: 'user' });
-
-            return {
-                own: user.id === this.userId,
-                content: feedback.attributes.feedback,
-                chdate: feedback.attributes.chdate,
-                mkdate: feedback.attributes.mkdate,
-                user_name: user?.attributes?.['formatted-name'] ?? '',
-                user_avatar: user?.meta?.avatar.small,
-            };
-        },
-        async loadFeedback() {
-            const parent = {
-                type: this.block.type,
-                id: this.block.id,
-            };
-            await this.loadRelatedFeedback({
-                parent,
-                relationship: 'feedback',
-                options: {
-                    include: 'user',
-                },
-            });
-        },
-        async postFeedback() {
-            this.updateSrMessage(this.$gettext('Feedback gesendet'));
-            const data = {
-                attributes: {
-                    feedback: this.feedbackText,
-                },
-                relationships: {
-                    block: {
-                        data: {
-                            type: this.block.type,
-                            id: this.block.id,
-                        },
-                    },
-                },
-            };
-            await this.createFeedback(data, { root: true });
-            this.feedbackText = '';
-            this.loadFeedback();
-        },
-        updateSrMessage(message) {
-            this.srMessage = '';
-            this.srMessage = message;
-        }
-    },
-    async mounted() {
-        await this.loadFeedback(this.block.id);
-    },
-    updated() {
-        this.$refs.feedbacks.scrollTop = this.$refs.feedbacks.scrollHeight;
-    },
-    watch: {
-        feedback() {
-            if (this.feedback && this.feedback.length > 0) {
-                this.$emit('hasFeedback');
-            }
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockHelper.vue b/resources/vue/components/courseware/CoursewareBlockHelper.vue
deleted file mode 100644
index 1a709f8..0000000
--- a/resources/vue/components/courseware/CoursewareBlockHelper.vue
+++ /dev/null
@@ -1,256 +0,0 @@
-<template>
-    <div class="block-helper">
-        <courseware-companion-box :msgCompanion="currentQuestion.text" :mood="companionMood"/>
-        <div v-if="showBlocks" class="cw-block-helper-results cw-element-adder-wrapper ">
-            <courseware-blockadder-item
-                v-for="(block, index) in selectedBlockTypes"
-                :key="index"
-                :title="block.title"
-                :type="block.type"
-                :description="block.description"
-                @blockAdded="resetQuestions"
-            />
-        </div>
-        <div class="cw-block-helper-buttons">
-            <button
-                v-for="(response, index) in currentQuestion.responses"
-                class="button"
-                :key="index"
-                @click="setQuestion(response.answer)"
-            >
-                {{ response.text }}
-            </button>
-
-            <button v-if="currentQuestionId !== 'a'" class="button cw-block-helper-reset" @click="resetQuestions">
-                <translate>Zurücksetzen</translate>
-            </button>
-        </div>
-    </div>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue';
-import { mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-block-helper',
-    components: {
-        CoursewareCompanionBox,
-        CoursewareBlockadderItem,
-    },
-
-    data() {
-        return {
-            companionMood: 'pointing',
-            questions: [
-                {
-                    id: 'a',
-                    text: this.$gettext(
-                        'Ich helfe Ihnen bei der Auswahl des richtigen Blocks. Beantworten Sie mir einfach ein paar Fragen. Meine Vorschläge werden dann hier anzeigen.'
-                    ),
-                    responses: [
-                        {
-                            text: this.$gettext('Ok'),
-                            answer: 'b',
-                        },
-                    ],
-                    blockChooser: false,
-                },
-                {
-                    id: 'b',
-                    text: this.$gettext('Kommt der Inhalt von einer anderen Plattform, z.B. Youtube?'),
-                    responses: [
-                        {
-                            text: this.$gettext('Ja'),
-                            answer: 'c',
-                        },
-                        {
-                            text: this.$gettext('Nein'),
-                            answer: 'd',
-                        },
-                    ],
-                    blockChooser: false,
-                },
-                {
-                    id: 'c',
-                    text: this.$gettext('Prima! Hier sind meine Vorschläge.'),
-                    responses: [],
-                    blockChooser: { category: 'external', fileType: false },
-                },
-                {
-                    id: 'd',
-                    text: this.$gettext('Enthält der Inhalt eine oder mehrere Dateien?'),
-                    responses: [
-                        {
-                            text: this.$gettext('Ja'),
-                            answer: 'e',
-                        },
-                        {
-                            text: this.$gettext('Nein'),
-                            answer: 'f',
-                        },
-                    ],
-                    blockChooser: false,
-                },
-                {
-                    id: 'f',
-                    text: this.$gettext('Handelt es sich bei dem Inhalt hauptsächlich um Text?'),
-                    responses: [
-                        {
-                            text: this.$gettext('Ja'),
-                            answer: 'g',
-                        },
-                        {
-                            text: this.$gettext('Nein'),
-                            answer: 'h',
-                        },
-                    ],
-                    blockChooser: false,
-                },
-                {
-                    id: 'g',
-                    text: this.$gettext('Prima! Hier sind meine Vorschläge.'),
-                    responses: [],
-                    blockChooser: { category: 'text', fileType: false },
-                },
-                {
-                    id: 'h',
-                    text: this.$gettext('Prima! Hier sind meine Vorschläge.'),
-                    responses: [],
-                    blockChooser: { category: 'layout', fileType: false },
-                },
-                {
-                    id: 'e',
-                    text: this.$gettext('Um weleche Art von Datei(en) handelt es sich?'),
-                    responses: [
-                        {
-                            text: this.$gettext('Audio'),
-                            answer: 'i',
-                        },
-                        {
-                            text: this.$gettext('Bild'),
-                            answer: 'j',
-                        },
-                        {
-                            text: this.$gettext('Dokument'),
-                            answer: 'k',
-                        },
-                        {
-                            text: this.$gettext('Video'),
-                            answer: 'l',
-                        },
-                        {
-                            text: this.$gettext('beliebig'),
-                            answer: 'm',
-                        },
-                    ],
-                    blockChooser: false,
-                },
-                {
-                    id: 'i',
-                    text: this.$gettext('Prima! Hier sind meine Vorschläge.'),
-                    responses: [],
-                    blockChooser: { category: false, fileType: 'audio' },
-                },
-                {
-                    id: 'j',
-                    text: this.$gettext('Prima! Hier sind meine Vorschläge.'),
-                    responses: [],
-                    blockChooser: { category: false, fileType: 'image' },
-                },
-                {
-                    id: 'k',
-                    text: this.$gettext('Prima! Hier sind meine Vorschläge.'),
-                    responses: [],
-                    blockChooser: { category: false, fileType: 'document' },
-                },
-                {
-                    id: 'l',
-                    text: this.$gettext('Prima! Hier sind meine Vorschläge.'),
-                    responses: [],
-                    blockChooser: { category: false, fileType: 'video' },
-                },
-                {
-                    id: 'm',
-                    text: this.$gettext('Prima! Hier sind meine Vorschläge.'),
-                    responses: [],
-                    blockChooser: { category: false, fileType: 'all' },
-                },
-            ],
-            currentQuestionId: 'a',
-            showBlocks: false,
-            selectedBlockTypes: [],
-        };
-    },
-    computed: {
-        ...mapGetters({ blockTypes: 'blockTypes' }),
-        currentQuestion() {
-            let question = {};
-            let view = this;
-            this.questions.forEach((q) => {
-                if (q.id === view.currentQuestionId) {
-                    question = q;
-                }
-            });
-            return question;
-        },
-    },
-    methods: {
-        blockChooser(choice) {
-            if (choice.category) {
-                this.setSelectedBlockTypesByCategory(choice.category);
-                this.showBlocks = true;
-            } else if (choice.fileType) {
-                this.setSelectedBlockTypesByFileTypes(choice.fileType);
-                this.showBlocks = true;
-            }
-        },
-        setQuestion(q) {
-            this.currentQuestionId = q;
-            if(this.currentQuestion.responses.length === 0) {
-                this.companionMood= 'special';
-            } else {
-                this.companionMood= 'unsure';
-            }
-        },
-        setSelectedBlockTypesByCategory(cat) {
-            this.selectedBlockTypes = [];
-
-            this.blockTypes.forEach((block) => {
-                if (block.categories.includes(cat)) {
-                    this.selectedBlockTypes.push(block);
-                }
-            });
-            this.selectedBlockTypes.sort((a, b) => {
-                return a.title > b.title ? 1 : b.title > a.title ? -1 : 0;
-            });
-        },
-        setSelectedBlockTypesByFileTypes(type) {
-            this.selectedBlockTypes = [];
-
-            this.blockTypes.forEach((block) => {
-                if (type === 'all' && block.file_types.length > 0) {
-                    this.selectedBlockTypes.push(block);
-                } else if (block.file_types.includes(type)) {
-                    this.selectedBlockTypes.push(block);
-                }
-            });
-            this.selectedBlockTypes.sort((a, b) => {
-                return a.title > b.title ? 1 : b.title > a.title ? -1 : 0;
-            });
-        },
-        resetQuestions() {
-            this.currentQuestionId = 'a';
-            this.showBlocks = false;
-            this.selectedBlockTypes = [];
-            this.companionMood= 'pointing';
-        },
-    },
-    watch: {
-        currentQuestionId() {
-            this.blockChooser(this.currentQuestion.blockChooser);
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockInfo.vue b/resources/vue/components/courseware/CoursewareBlockInfo.vue
deleted file mode 100644
index 17a7165..0000000
--- a/resources/vue/components/courseware/CoursewareBlockInfo.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-    <section class="cw-block-info">
-        <header>{{ $gettext('Informationen') }}</header>
-        <div class="cw-block-features-content cw-block-info-content">
-            <table class="cw-block-info-table">
-                <tr>
-                    <td>{{ $gettext('Blockbeschreibung') }}</td>
-                    <td><slot name="info" /></td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Block wurde erstellt von') }}</td>
-                    <td>{{ owner }}</td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Block wurde erstellt am') }}:</td>
-                    <td><iso-date :date="block.attributes.mkdate" /></td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Zuletzt bearbeitet von') }}:</td>
-                    <td>{{ editor }}</td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Zuletzt bearbeitet am') }}:</td>
-                    <td><iso-date :date="block.attributes.chdate" /></td>
-                </tr>
-            </table>
-            <button class="button" @click="$emit('close')">{{ $gettext('Schließen') }}</button>
-        </div>
-    </section>
-</template>
-
-<script>
-import IsoDate from './IsoDate.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-block-info',
-    components: { IsoDate },
-    props: {
-        block: Object,
-    },
-    computed: {
-        ...mapGetters({
-            relatedUsers: 'users/related',
-        }),
-        owner() {
-            const owner = this.relatedUsers({
-                parent: this.block,
-                relationship: 'owner',
-            });
-
-            return owner?.attributes['formatted-name'] ?? '';
-        },
-
-        editor() {
-            const editor = this.relatedUsers({
-                parent: this.block,
-                relationship: 'editor',
-            });
-
-            return editor?.attributes['formatted-name'] ?? '';
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockadderItem.vue b/resources/vue/components/courseware/CoursewareBlockadderItem.vue
deleted file mode 100644
index 921014a..0000000
--- a/resources/vue/components/courseware/CoursewareBlockadderItem.vue
+++ /dev/null
@@ -1,115 +0,0 @@
-<template>
-    <div class="cw-blockadder-item-wrapper">
-        <a href="#" @click.prevent="addBlock" class="cw-blockadder-item" :class="['cw-blockadder-item-' + type]">
-            <header class="cw-blockadder-item-title">
-                {{ title }}
-            </header>
-            <p class="cw-blockadder-item-description">
-                {{ description }}
-            </p>
-        </a>
-        <button
-            class="cw-blockadder-item-fav"
-            :title="favButtonTitle"
-            @click="toggleFavItem()"
-        >
-            <studip-icon :shape="blockTypeIsFav ? 'star' : 'star-empty'" :size="20" />
-        </button>
-    </div>
-    
-</template>
-
-<script>
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-blockadder-item',
-    components: {},
-    props: {
-        title: String,
-        description: String,
-        type: String,
-    },
-    data() {
-        return {
-            showInfo: false,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            blockAdder: 'blockAdder',
-            blockById: 'courseware-blocks/byId',
-            lastCreatedBlock: 'courseware-blocks/lastCreated',
-            favoriteBlockTypes: 'favoriteBlockTypes',
-        }),
-        blockTypeIsFav() {
-            return this.favoriteBlockTypes.some((type) => type.type === this.type);
-        },
-        favButtonTitle() {
-            if (this.blockTypeIsFav) {
-                return this.$gettextInterpolate(
-                    this.$gettext('%{ blockName } Block aus den Favoriten entfernen'),
-                    { blockName: this.title }
-                );
-            }
-
-            return this.$gettextInterpolate(
-                    this.$gettext('%{ blockName } Block zu Favoriten hinzufügen'),
-                    { blockName: this.title }
-                );   
-        }
-    },
-    methods: {
-        ...mapActions({
-            companionInfo: 'companionInfo',
-            companionSuccess: 'companionSuccess',
-            companionWarning: 'companionWarning',
-            createBlock: 'createBlockInContainer',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-            loadBlock: 'courseware-blocks/loadById',
-            updateContainer: 'updateContainer',
-            removeFavoriteBlockType: 'removeFavoriteBlockType',
-            addFavoriteBlockType: 'addFavoriteBlockType',
-        }),
-        async addBlock() {
-            if (Object.keys(this.blockAdder).length !== 0) {
-                // lock parent container
-                await this.lockObject({ id: this.blockAdder.container.id, type: 'courseware-containers' });
-                // create new block
-                await this.createBlock({
-                    container: this.blockAdder.container,
-                    section: this.blockAdder.section,
-                    blockType: this.type,
-                });
-                //get new Block
-                const newBlock = this.lastCreatedBlock;
-                // update container information -> new block id in sections
-                let container = this.blockAdder.container;
-                container.attributes.payload.sections[this.blockAdder.section].blocks.push(newBlock.id);
-                const structuralElementId = container.relationships['structural-element'].data.id;
-                // update container
-                await this.updateContainer({ container, structuralElementId });
-                // unlock container
-                await this.unlockObject({ id: this.blockAdder.container.id, type: 'courseware-containers' });
-                this.companionSuccess({
-                    info: this.$gettext('Der Block wurde erfolgreich eingefügt.'),
-                });
-                this.$emit('blockAdded');
-            } else {
-                // companion action
-                this.companionWarning({
-                    info: this.$gettext('Bitte wählen Sie einen Ort aus, an dem der Block eingefügt werden soll.'),
-                });
-            }
-        },
-        toggleFavItem() {
-            if (this.blockTypeIsFav) {
-                this.removeFavoriteBlockType(this.type);
-            } else {
-                this.addFavoriteBlockType(this.type);
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/CoursewareCanvasBlock.vue
deleted file mode 100644
index e725798..0000000
--- a/resources/vue/components/courseware/CoursewareCanvasBlock.vue
+++ /dev/null
@@ -1,712 +0,0 @@
-<template>
-    <div class="cw-block cw-block-canvas" ref="block">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="currentTitle" class="cw-block-title">
-                    {{ currentTitle }}
-                </div>
-                <div class="cw-canvasblock-toolbar">
-                    <div class="cw-canvasblock-buttonset">
-                        <button class="cw-canvasblock-reset" :title="$gettext('Zurücksetzen')" @click="reset"></button>
-                        <button class="cw-canvasblock-undo" :title="$gettext('Rückgängig')" @click="undo"></button>
-                        <button v-if="hasUploadFolder" class="cw-canvasblock-store" :title="$gettext('Bild im Dateibereich speichern')" @click="store"></button>
-                        <button v-if="canSwitchView" :class="viewButtonClass" :title="viewButtonText" @click="switchView"></button>
-                    </div>
-                    <div class="cw-canvasblock-buttonset">
-                        <button
-                            v-for="color in colors"
-                            :key="color.name"
-                            class="cw-canvasblock-color"
-                            :class="[currentColor === color.name ? 'selected-color' : '', color.name]"
-                            :title="color.title"
-                            @click="setColor(color.name)"
-                        />
-                    </div>
-                    <div class="cw-canvasblock-buttonset">
-                        <button
-                            class="cw-canvasblock-size cw-canvasblock-size-small"
-                            :class="{ 'selected-size': currentSize === 2 }"
-                            :title="$gettext('klein')"
-                            @click="setSize('small')"
-                        />
-                        <button
-                            class="cw-canvasblock-size cw-canvasblock-size-normal"
-                            :class="{ 'selected-size': currentSize === 5 }"
-                            :title="$gettext('normal')"
-                            @click="setSize('normal')"
-                        />
-                        <button
-                            class="cw-canvasblock-size cw-canvasblock-size-large"
-                            :class="{ 'selected-size': currentSize === 8 }"
-                            :title="$gettext('groß')"
-                            @click="setSize('large')"
-                        />
-                        <button
-                            class="cw-canvasblock-size cw-canvasblock-size-huge"
-                            :class="{ 'selected-size': currentSize === 12 }"
-                            :title="$gettext('riesig')"
-                            @click="setSize('huge')"
-                        />
-                    </div>
-                    <div class="cw-canvasblock-buttonset">
-                        <button
-                            class="cw-canvasblock-tool cw-canvasblock-tool-pen"
-                            :class="{ 'selected-tool': currentTool === 'pen' }"
-                            :title="$gettext('Zeichenwerkzeug')"
-                            @click="setTool('pen')"
-                        />
-                        <button
-                            class="cw-canvasblock-tool cw-canvasblock-tool-text"
-                            :class="{ 'selected-tool': currentTool === 'text' }"
-                            :title="$gettext('Textwerkzeug')"
-                            @click="setTool('text')"
-                        >
-                            T
-                        </button>
-                    </div>
-                </div>
-                <img :src="currentUrl" class="cw-canvasblock-original-img" ref="image" @load="buildCanvas" />
-                <input
-                    v-show="textInput"
-                    class="cw-canvasblock-text-input"
-                    ref="textInputField"
-                    @keyup="textInputKeyUp"
-                />
-                <canvas
-                    class="cw-canvasblock-canvas"
-                    :class="{
-                        'cw-canvasblock-tool-selected-pen': currentTool === 'pen',
-                        'cw-canvasblock-tool-selected-text': currentTool === 'text',
-                    }"
-                    ref="canvas"
-                    @mousedown="mouseDown"
-                    @mousemove="mouseMove"
-                    @mouseup="mouseUp"
-                    @mouseout="mouseUp"
-                    @mouseleave="mouseUp"
-
-                    @touchstart="touchStart"
-                    @touchmove="touchMove"
-                    @touchend="touchEnd"
-                />
-                <div class="cw-canvasblock-hints">
-                    <div v-show="write" class="messagebox messagebox_info cw-canvasblock-text-info">
-                        {{ $gettext('Texteingabe mit Enter-Taste bestätigen') }}
-                    </div>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <courseware-tabs>
-                    <courseware-tab
-                        :index="0"
-                        :name="$gettext('Grunddaten')"
-                        :selected="true"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Überschrift') }}
-                                <input type="text" v-model="currentTitle" />
-                            </label>
-                            <label>
-                                {{ $gettext('Hintergrundbild') }}
-                                <select v-model="currentImage">
-                                    <option value="true">{{ $gettext('Ja') }}</option>
-                                    <option value="false">{{ $gettext('Nein') }}</option>
-                                </select>
-                            </label>
-                            <label v-if="currentImage === 'true'">
-                                {{ $gettext('Bilddatei') }}
-                                <courseware-file-chooser
-                                    v-model="currentFileId"
-                                    :isImage="true"
-                                    @selectFile="updateCurrentFile"
-                                />
-                            </label>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="1"
-                        :name="$gettext('Einstellungen')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Speicherort') }}
-                                <courseware-folder-chooser v-model="currentUploadFolderId" :unchoose="true"/>
-                            </label>
-                            <label>
-                                {{ $gettext('Werte anderer Nutzer anzeigen') }}
-                                <select v-model="currentShowUserData">
-                                    <option value="off">{{ $gettext('deaktiviert') }}</option>
-                                    <option value="teacher">{{ $gettext('nur für Lehrende') }}</option>
-                                    <option value="all">{{ $gettext('für alle') }}</option>
-                                </select>
-                            </label>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Leinwand-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFileChooser from './CoursewareFileChooser.vue';
-import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-canvas-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareFileChooser,
-        CoursewareFolderChooser,
-        CoursewareTabs,
-        CoursewareTab,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentImage: '',
-            currentFileId: '',
-            currentUploadFolderId: '',
-            currentShowUserData: '',
-            currentUserView: 'own',
-            currentFile: {},
-
-            context: {},
-            paint: false,
-            write: false,
-            clickX: [],
-            clickY: [],
-            clickDrag: [],
-            clickColor: [],
-            colors: [
-                {rgba: 'rgba(255,255,255,1)', title: this.$gettext('weiß'), name: 'white'},
-                {rgba: 'rgba(52,152,219,1)', title: this.$gettext('blau'), name: 'blue'},
-                {rgba: 'rgba(46,204,113,1)', title: this.$gettext('grün'), name: 'green'},
-                {rgba: 'rgba(155,89,182,1)', title: this.$gettext('lila'), name: 'purple'},
-                {rgba: 'rgba(231,76,60,1)', title: this.$gettext('rot'), name: 'red'},
-                {rgba: 'rgba(254,211,48,1)', title: this.$gettext('gelb'), name: 'yellow'},
-                {rgba: 'rgba(243,156,18,1)', title: this.$gettext('orange'), name: 'orange'},
-                {rgba: 'rgba(149,165,166,1)', title: this.$gettext('grau'), name: 'grey'},
-                {rgba: 'rgba(52,73,94,1)', title: this.$gettext('dunkel grau'), name: 'darkgrey'},
-                {rgba: 'rgba(0,0,0,1)', title: this.$gettext('schwarz'), name: 'black'},
-            ],
-            currentColor: '',
-            currentColorRGBA: '',
-            sizes: { small: 2, normal: 5, large: 8, huge: 12 },
-            clickSize: [],
-            currentSize: '',
-            tools: { pen: 'pen', text: 'text' },
-            currentTool: '',
-            clickTool: [],
-            Text: [],
-            textInput: false,
-            file: null
-        };
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            getUserDataById: 'courseware-user-data-fields/byId',
-            usersById: 'users/byId',
-            relatedUserData: 'user-data-field/related'
-        }),
-        userData() {
-            return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id });
-        },
-        canvasDraw() {
-            if (this.userData !== undefined && this.userData.attributes.payload.canvas_draw) {
-                return this.userData.attributes.payload.canvas_draw;
-            } else {
-                return false;
-            }
-        },
-        allCanvasDraws() {
-            const parent = { type: 'courseware-blocks', id: this.block.id };
-            const relationship = 'user-data-field';
-            const userDataFields = this.relatedUserData({
-                parent: parent,
-                relationship: relationship,
-            });
-            let canvasDraws = [];
-            if (userDataFields?.length > 0) {
-                for (let userDataField of userDataFields) {
-                    // extracting the canvas draws of the other users.
-                    if (userDataField?.attributes?.payload?.canvas_draw &&
-                        userDataField?.relationships?.user?.data?.id !== this.userId ) {
-                        let canvas_draw = userDataField.attributes.payload.canvas_draw;
-                        let draw_obj = {
-                            clickX: JSON.parse(canvas_draw.clickX),
-                            clickY: JSON.parse(canvas_draw.clickY),
-                            clickDrag: JSON.parse(canvas_draw.clickDrag),
-                            clickColor: JSON.parse(canvas_draw.clickColor),
-                            clickSize: JSON.parse(canvas_draw.clickSize),
-                            clickTool: JSON.parse(canvas_draw.clickTool),
-                            Text: JSON.parse(canvas_draw.Text),
-                        };
-                        canvasDraws.push(draw_obj);
-                    }
-                }
-            }
-            return canvasDraws;
-        },
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        image() {
-            return this.block?.attributes?.payload?.image;
-        },
-        fileId() {
-            return this.block?.attributes?.payload?.file_id;
-        },
-        uploadFolderId() {
-            return this.block?.attributes?.payload?.upload_folder_id;
-        },
-        showUsersData() {
-            return this.block?.attributes?.payload?.show_usersdata;
-        },
-        currentUrl() {
-            if (this.currentFile?.meta) {
-                return this.currentFile.meta['download-url'];
-            } else if(this.currentFile?.download_url) {
-                    return this.currentFile.download_url;
-            } else {
-                return '';
-            }
-        },
-        currentFileName() {
-            if (this.currentFile?.attributes?.name) {
-                return this.currentFile.attributes.name;
-            } else {
-                return this.currentTitle + '.jpg';
-            }
-        },
-        hasUploadFolder() {
-            return this.currentUploadFolderId !== "";
-        },
-        canSwitchView() {
-            // this feature is not something to offer in the Arbeitsplatz!
-            let context = this.$store.getters.context;
-            if (context.type !== 'courses') {
-                return false;
-            }
-            if (this.currentShowUserData === 'off') {
-                return false;
-            }
-            if (this.currentShowUserData === 'teacher' && !this.isTeacher) {
-                return false;
-            }
-            return true;
-        },
-        viewButtonText() {
-            let text = this.$gettext('Werte anderer Nutzer anzeigen');
-            if (this.currentUserView == 'own') {
-                text = this.$gettext('Nur eigene Werte anzeigen');
-            }
-            return text;
-        },
-        viewButtonClass() {
-            return 'cw-canvasblock-show-' + this.currentUserView;
-        }
-    },
-    mounted() {
-        this.loadFileRefs(this.block.id).then((response) => {
-            this.file = response[0];
-            this.currentFile = this.file;
-            this.initCurrentData();
-            this.buildCanvas();
-        });
-        this.loadImageFile();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            loadFileRefs: 'loadFileRefs',
-            createFile: 'createFile',
-            companionSuccess: 'companionSuccess',
-            companionError: 'companionError',
-            updateUserDataFields: 'courseware-user-data-fields/update',
-            loadUserDataFields: 'loadUserDataFields',
-        }),
-        initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentImage = this.image;
-            this.currentFileId = this.fileId;
-            this.currentUploadFolderId = this.uploadFolderId;
-            this.currentShowUserData = this.showUsersData;
-            if (this.canvasDraw) {
-                this.clickX = JSON.parse(this.canvasDraw.clickX);
-                this.clickY = JSON.parse(this.canvasDraw.clickY);
-                this.clickDrag = JSON.parse(this.canvasDraw.clickDrag);
-                this.clickColor = JSON.parse(this.canvasDraw.clickColor);
-                this.clickSize = JSON.parse(this.canvasDraw.clickSize);
-                this.clickTool = JSON.parse(this.canvasDraw.clickTool);
-                this.Text = JSON.parse(this.canvasDraw.Text);
-            }
-        },
-        loadImageFile() {
-            this.loadFileRefs(this.block.id).then((response) => {
-                this.file = response[0];
-                this.currentFile = this.file;
-                this.initCurrentData();
-                this.buildCanvas();
-            });
-        },
-        updateCurrentFile(file) {
-            this.currentFile = file;
-            this.currentFileId = file.id;
-            this.buildCanvas();
-        },
-        setColor(color) {
-            if (this.write) {
-                return;
-            }
-            this.currentColor = color;
-            this.currentColorRGBA = this.colors.find(c => c.name === color).rgba;
-        },
-        setSize(size) {
-            if (this.textInput) {
-                return;
-            }
-            this.currentSize = this.sizes[size];
-        },
-        setTool(tool) {
-            if (this.write) {
-                this.clickX.pop();
-                this.clickY.pop();
-                this.clickDrag.pop();
-                this.clickColor.pop();
-                this.clickSize.pop();
-                this.clickTool.pop();
-                this.write = false;
-                this.textInput = false;
-            }
-            this.currentTool = this.tools[tool];
-        },
-        reset() {
-            this.clickX.length = 0;
-            this.clickY.length = 0;
-            this.clickDrag.length = 0;
-            this.clickColor.length = 0;
-            this.clickSize.length = 0;
-            this.clickTool.length = 0;
-            this.Text.length = 0;
-            this.paint = false;
-            this.write = false;
-            this.textInput = false;
-            this.redraw();
-        },
-        buildCanvas() {
-            let blockElem = this.$refs.block;
-            let image = this.$refs.image;
-            let canvas = this.$refs.canvas;
-            canvas.width = blockElem.offsetWidth - 2;
-            if (this.currentImage === 'true' && image.height > 0) {
-                canvas.height = Math.round((canvas.width / image.width) * image.height);
-            } else {
-                canvas.height = 500;
-            }
-            this.context = canvas.getContext('2d');
-            this.setColor('blue');
-            this.currentSize = this.sizes['normal'];
-            this.currentTool = this.tools['pen'];
-            this.redraw();
-        },
-        redraw() {
-            let view = this;
-            let context = view.context;
-            context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
-            context.fillStyle = '#ffffff';
-            context.fillRect(0, 0, context.canvas.width, context.canvas.height); // set background
-            if (view.currentImage === 'true') {
-                let outlineImage = new Image();
-                outlineImage.src = this.currentUrl;
-                context.drawImage(outlineImage, 0, 0, context.canvas.width, context.canvas.height);
-            }
-
-            context.lineJoin = 'round';
-            let ownCanvasDraw = {
-                clickX: view.clickX,
-                clickY: view.clickY,
-                clickDrag: view.clickDrag,
-                clickColor: view.clickColor,
-                clickSize: view.clickSize,
-                clickTool: view.clickTool,
-                Text: view.Text
-            }
-            let canvasDraws = [ownCanvasDraw];
-            if (this.currentUserView === 'all') {
-                canvasDraws = [ ...canvasDraws, ...view.allCanvasDraws ];
-            }
-
-            for (let draw of canvasDraws) {
-                for (var j = 0; j < draw.clickX.length; j++) {
-                    if (draw.clickTool[j] === 'pen') {
-                        context.beginPath();
-                        if (draw.clickDrag[j] && j) {
-                            context.moveTo(draw.clickX[j - 1], draw.clickY[j - 1]);
-                        } else {
-                            context.moveTo(draw.clickX[j] - 1, draw.clickY[j]);
-                        }
-                        context.lineTo(draw.clickX[j], draw.clickY[j]);
-                        context.closePath();
-                        context.strokeStyle = draw.clickColor[j];
-                        context.lineWidth = draw.clickSize[j];
-                        context.stroke();
-                    }
-                    if (draw.clickTool[j] === 'text') {
-                        let fontsize = draw.clickSize[j] * 6;
-                        context.font = fontsize + 'px Arial ';
-                        context.fillStyle = draw.clickColor[j];
-                        context.fillText(draw.Text[j], draw.clickX[j], draw.clickY[j] + fontsize);
-                    }
-                }
-            }
-        },
-        mouseDown(e) {
-            if (this.write) {
-                let view = this;
-                this.$refs.textInputField.focus();
-                window.setTimeout(function () {
-                    view.$refs.textInputField.focus();
-                }, 0);
-                return;
-            }
-            if (this.currentTool === 'pen') {
-                this.paint = true;
-                this.addClick(e.offsetX, e.offsetY, false);
-                this.redraw();
-            }
-            if (this.currentTool === 'text') {
-                this.write = true;
-                this.addClick(e.offsetX, e.offsetY, false);
-            }
-        },
-        mouseMove(e) {
-            if (this.paint) {
-                this.addClick(e.offsetX, e.offsetY, true);
-                this.redraw();
-            }
-        },
-        mouseUp(e) {
-            this.storeDraw();
-            this.paint = false;
-        },
-        touchStart(e) {
-            e.preventDefault();
-            if (this.write) {
-                return;
-            }
-            let canvas = this.$refs.canvas;
-            let mousePos = this.getTouchPos(canvas, e);
-            if(this.currentTool == 'pen') {
-                this.paint = true;
-                this.addClick(mousePos.x, mousePos.y, false);
-                this.redraw();
-            }
-            if(this.currentTool == 'text') {
-                this.write = true;
-                this.addClick(mousePos.x, mousePos.y, false);
-            }
-        },
-        touchMove(e) {
-            e.preventDefault();
-
-            let canvas = this.$refs.canvas;
-            let mousePos = this.getTouchPos(canvas, e);
-            if(this.paint){
-                this.addClick(mousePos.x, mousePos.y, true);
-                this.redraw();
-            }
-        },
-        touchEnd(e) {
-            this.storeDraw();
-            this.paint = false;
-        },
-        getTouchPos(canvasDom, touchEvent) {
-            var rect = canvasDom.getBoundingClientRect();
-            return {
-                x: touchEvent.touches[0].clientX - rect.left,
-                y: touchEvent.touches[0].clientY - rect.top
-            };
-        },
-        addClick(x, y, dragging) {
-            this.clickX.push(x);
-            this.clickY.push(y);
-            this.clickDrag.push(dragging);
-            this.clickColor.push(this.currentColorRGBA);
-            this.clickSize.push(this.currentSize);
-            this.clickTool.push(this.currentTool);
-            if (this.currentTool === 'text') {
-                this.enableTextInput(x, y);
-            } else {
-                this.Text.push('');
-            }
-        },
-        undo() {
-            let dragging = this.clickDrag[this.clickDrag.length - 1];
-            this.clickX.pop();
-            this.clickY.pop();
-            this.clickDrag.pop();
-            this.clickColor.pop();
-            this.clickSize.pop();
-            this.clickTool.pop();
-            if (this.write) {
-                this.textInput = false;
-                this.write = false;
-            } else {
-                this.Text.pop('');
-            }
-            if (dragging) {
-                this.undo();
-            }
-            this.redraw();
-        },
-        enableTextInput(x, y) {
-            let view = this;
-            let fontsize = this.currentSize * 6;
-            this.textInput = true;
-            let input = this.$refs.textInputField;
-            input.value = '';
-            input.style.position = 'absolute';
-            input.style.top = this.$refs.canvas.offsetTop + y + 'px';
-            input.style.left = 320 + x + 'px';
-            input.style.lineHeight = fontsize + 'px';
-            input.style.fontSize = fontsize + 'px';
-            input.style.width = '300px';
-            window.setTimeout(function () {
-                view.$refs.textInputField.focus();
-            }, 0);
-        },
-        textInputKeyUp(e) {
-            if (e.defaultPrevented) {
-                return;
-            }
-            let key = e.key || e.keyCode;
-            if (key === 'Enter' || key === 13) {
-                this.Text.push(this.$refs.textInputField.value);
-                this.textInput = false;
-                this.write = false;
-                this.redraw();
-            }
-            if (key === 'Escape' || key === 'Esc' || key === 27) {
-                this.clickX.pop();
-                this.clickY.pop();
-                this.clickDrag.pop();
-                this.clickColor.pop();
-                this.clickSize.pop();
-                this.clickTool.pop();
-                this.textInput = false;
-                this.write = false;
-            }
-        },
-        async storeDraw() {
-            let data = {};
-            data.type = 'courseware-user-data-fields';
-            data.id = this.block.relationships['user-data-field'].data.id;
-            data.relationships = {};
-            data.relationships.block = {};
-            data.relationships.block.data = {};
-            data.relationships.block.data.id = this.block.id;
-            data.relationships.block.data.type = this.block.type;
-            data.attributes = {};
-            data.attributes.payload = {};
-            data.attributes.payload.canvas_draw = {};
-            data.attributes.payload.canvas_draw.clickX = JSON.stringify(this.clickX);
-            data.attributes.payload.canvas_draw.clickY = JSON.stringify(this.clickY);
-            data.attributes.payload.canvas_draw.clickDrag = JSON.stringify(this.clickDrag);
-            data.attributes.payload.canvas_draw.clickColor = JSON.stringify(this.clickColor);
-            data.attributes.payload.canvas_draw.clickSize = JSON.stringify(this.clickSize);
-            data.attributes.payload.canvas_draw.clickTool = JSON.stringify(this.clickTool);
-            data.attributes.payload.canvas_draw.Text = JSON.stringify(this.Text);
-
-            await this.updateUserDataFields(data);
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.title = this.currentTitle;
-            attributes.payload.image = this.currentImage;
-            if (this.currentImage === 'true') {
-                attributes.payload.file_id = this.currentFileId;
-            } else {
-                attributes.payload.file_id = '';
-            }
-            attributes.payload.upload_folder_id = this.currentUploadFolderId;
-            attributes.payload.show_usersdata = this.currentShowUserData;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-        async store() {
-            let user = this.usersById({id: this.userId});
-            let imageBase64 = this.context.canvas.toDataURL("image/jpeg", 1.0);
-            let image = await fetch(imageBase64);
-            let imageBlob = await image.blob();
-            let file = {};
-            file.attributes = {};
-            if(this.currentImage === 'true') {
-                file.attributes.name = (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '_' + this.currentFile.attributes.name;
-            } else {
-                file.attributes.name = (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '_' + this.block.attributes.title + '_' + this.block.id;
-            }
-
-            let img = await this.createFile({
-                file: file,
-                filedata: imageBlob,
-                folder: {id: this.currentUploadFolderId}
-            });
-
-            if(img && img.type === 'file-refs') {
-                this.companionSuccess({
-                    info: this.$gettext('Das Bild wurde erfolgreich im Dateibereich abgelegt.')
-                });
-            } else {
-                this.companionError({
-                    info: this.$gettext('Es ist ein Fehler aufgetretten! Das Bild konnte nicht gespeichert werden.')
-                });
-            }
-        },
-        async switchView() {
-            if (['own', 'all'].includes(this.currentUserView)) {
-                let newView = 'own';
-                if (this.currentUserView === 'own') {
-                    // we will get the latest draws by loading them each time the view is going to be switched to all!
-                    await this.loadUserDataFields(this.block.id).then(() => newView = 'all');
-                }
-                this.currentUserView = newView;
-            }
-        }
-    },
-    watch: {
-        currentUserView() {
-            this.redraw();
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareChartBlock.vue b/resources/vue/components/courseware/CoursewareChartBlock.vue
deleted file mode 100644
index 4d1c6dd..0000000
--- a/resources/vue/components/courseware/CoursewareChartBlock.vue
+++ /dev/null
@@ -1,333 +0,0 @@
-<template>
-    <div class="cw-block cw-block-chart">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <canvas class="cw-chart-block-canvas" ref="chartCanvas" />
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label class="col-3">
-                        {{ $gettext('Beschriftung') }}
-                        <input type="text" v-model="currentLabel" @focusout="buildChart" />
-                    </label>
-                    <label class="col-3">
-                        {{ $gettext('Typ') }}
-                        <studip-select
-                            v-model="currentType"
-                            :options="chartTypes"
-                            :reduce="chartTypes => chartTypes.value"
-                            :clearable="false"
-                            @option:selected="buildChart"
-                        >
-                            <template #open-indicator="selectAttributes">
-                                <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
-                            </template>
-                            <template #selected-option="{name}">
-                                <span>{{name}}</span>
-                            </template>
-                            <template #option="{name}">
-                                <span>{{name}}</span>
-                            </template>
-                        </studip-select>
-                    </label>
-                </form>
-                <button class="button add" @click="addItem">{{ $gettext('Datensatz hinzufügen') }}</button>
-                <courseware-tabs
-                    v-if="currentContent.length > 0"
-                    :setSelected="setItemTab"
-                    @selectTab="setItemTab = (parseInt($event.name.replace($gettext('Datensatz') +  ' ', ''), 10) - 1)"
-                >
-                    <courseware-tab
-                        v-for="(item, index) in currentContent"
-                        :key="index"
-                        :index="index"
-                        :name="$gettext('Datensatz') +  ' ' + (index + 1).toString()"
-                        :selected="index === 0"
-                        canBeEmpty
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label class="col-1">
-                                {{ $gettext('Wert') }}
-                                <input type="number" v-model="item.value" @change="buildChart" />
-                            </label>
-                            <label class="col-2">
-                                {{ $gettext('Farbe') }}
-                                <studip-select
-                                    :options="colors"
-                                    :reduce="colors => colors.value"
-                                    label="rgb"
-                                    :clearable="false"
-                                    v-model="item.color"
-                                    @option:selected="buildChart"
-                                >
-                                    <template #open-indicator="selectAttributes">
-                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
-                                    </template>
-                                    <template #no-options>
-                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                                    </template>
-                                    <template #selected-option="{name, rgb}">
-                                        <span class="vs__option-color" :style="{'background-color': 'rgb(' + rgb + ')'}"></span><span>{{name}}</span>
-                                    </template>
-                                    <template #option="{name, rgb}">
-                                        <span class="vs__option-color" :style="{'background-color': 'rgb(' + rgb + ')'}"></span><span>{{name}}</span>
-                                    </template>
-                                </studip-select>
-                            </label>
-                            <label class="col-3">
-                                {{ $gettext('Bezeichnung') }}
-                                <input type="text" v-model="item.label" @focusout="buildChart" />
-                            </label>
-                            <button
-                                v-if="currentContent.length > 1"
-                                class="button trash"
-                                @click="removeItem(index)"
-                            >
-                                {{ $gettext('Datensatz entfernen') }}
-                            </button>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>                
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Diagramm-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import StudipIcon from '../StudipIcon.vue';
-import { blockMixin } from './block-mixin.js';
-import Chart from 'chart.js';
-import { mapActions } from 'vuex';
-
-
-export default {
-    name: 'courseware-chart-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareTabs,
-        CoursewareTab,
-        StudipIcon,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            chart: null,
-            currentContent: [],
-            currentLabel: '',
-            currentType: '',
-            colors: [
-                { name:this.$gettext('Rot'), value: 'red', rgb: '192, 57, 43' },
-                { name:this.$gettext('Blau'), value: 'blue', rgb: '52, 152, 219' },
-                { name:this.$gettext('Gelb'), value: 'yellow', rgb: '241, 196, 15' },
-                { name:this.$gettext('Grün'), value: 'green', rgb: '46, 204, 113' },
-                { name:this.$gettext('Lila'), value: 'purple', rgb: '155, 89, 182' },
-                { name:this.$gettext('Orange'), value: 'orange', rgb: '230, 126, 34' },
-                { name:this.$gettext('Türkis'), value: 'turquoise', rgb: '26, 188, 156' },
-                { name:this.$gettext('Grau'), value: 'grey', rgb: '52, 73, 94' },
-                { name:this.$gettext('Hellgrau'), value: 'lightgrey', rgb: '149, 165, 166' },
-                { name:this.$gettext('Schwarz'), value: 'black', rgb: '0, 0, 0' },
-            ],
-            chartTypes: [
-                { name: this.$gettext('Säulendiagramm'), value: 'bar'},
-                { name: this.$gettext('Balkendiagramm'), value: 'horizontalBar'},
-                { name: this.$gettext('Kreisdiagramm'), value: 'pie'},
-                { name: this.$gettext('Ringdiagramm'), value: 'doughnut'},
-                { name: this.$gettext('Polardiagramm'), value: 'polarArea'},
-                { name: this.$gettext('Liniendiagramm'), value: 'line'},
-            ],
-            textRecordRemove: this.$gettext('Datensatz entfernen'),
-            setItemTab: 0,
-        };
-    },
-    computed: {
-        content() {
-            return this.block?.attributes?.payload?.content;
-        },
-        label() {
-            return this.block?.attributes?.payload?.label;
-        },
-        type() {
-            return this.block?.attributes?.payload?.type;
-        },
-        onlyRecord() {
-            return this.currentContent.length === 1;
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            this.currentContent = this.content;
-            this.currentLabel = this.label;
-            this.currentType = this.type;
-            this.setItemTab = 0;
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.content = this.currentContent;
-            attributes.payload.label = this.currentLabel;
-            attributes.payload.type = this.currentType;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-
-        addItem() {
-            this.currentContent.push({ value: '0', label: '', color: 'blue' });
-            this.$nextTick(() => { this.setItemTab = this.currentContent.length - 1; });
-        },
-
-        removeItem(recordIndex) {
-            this.currentContent = this.currentContent.filter((val, index) => {
-                return !(index === recordIndex);
-            });
-            this.buildChart();
-            this.$nextTick(() => { this.setItemTab = 0; });
-        },
-
-        buildChart() {
-            if (this.chart !== null) {
-                this.chart.destroy();
-            }
-            let ctx = this.$refs.chartCanvas.getContext('2d');
-            let type = this.currentType;
-            let label = this.currentLabel;
-            let labels = [];
-            let data = [];
-            let backgroundColor = [];
-            let borderColor = [];
-
-            this.currentContent.forEach((item) => {
-                labels.push(item.label);
-                data.push(item.value);
-                backgroundColor.push('rgba(' + this.colors.filter((color) => { return color.value === item.color })[0].rgb + ', 0.6)');
-                borderColor.push('rgba(' + this.colors.filter((color) => { return color.value === item.color })[0].rgb + ', 1.0)');
-            });
-
-            switch (type) {
-                case 'bar':
-                case 'horizontalBar':
-                    this.chart = new Chart(ctx, {
-                        type: type,
-                        data: {
-                            labels: labels,
-                            datasets: [
-                                {
-                                    label: label,
-                                    data: data,
-                                    backgroundColor: backgroundColor,
-                                    borderColor: borderColor,
-                                    borderWidth: 1,
-                                },
-                            ],
-                        },
-                        options: {
-                            scales: {
-                                yAxes: [
-                                    {
-                                        ticks: {
-                                            beginAtZero: true,
-                                        },
-                                    },
-                                ],
-                                xAxes: [
-                                    {
-                                        ticks: {
-                                            beginAtZero: true,
-                                        },
-                                    },
-                                ],
-                            },
-                            legend: {
-                                display: false,
-                            },
-                            title: {
-                                display: true,
-                                text: label,
-                            },
-                        },
-                    });
-                    break;
-                case 'pie':
-                case 'doughnut':
-                case 'polarArea':
-                    this.chart = new Chart(ctx, {
-                        type: type,
-                        data: {
-                            labels: labels,
-                            datasets: [
-                                {
-                                    data: data,
-                                    backgroundColor: backgroundColor,
-                                    borderWidth: 1,
-                                },
-                            ],
-                        },
-                        options: {
-                            title: {
-                                display: true,
-                                text: label,
-                            },
-                        },
-                    });
-                    break;
-                case 'line':
-                    this.chart = new Chart(ctx, {
-                        type: type,
-                        data: {
-                            labels: labels,
-                            datasets: [
-                                {
-                                    label: label,
-                                    data: data,
-                                    fill: false,
-                                    borderWidth: 2,
-                                    pointBackgroundColor: borderColor,
-                                },
-                            ],
-                        },
-                        options: {
-                            title: {
-                                display: true,
-                                text: label,
-                            },
-                        },
-                    });
-                    break;
-            }
-        },
-    },
-    watch: {
-        currentType() {
-            this.buildChart();
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareClipboardItem.vue b/resources/vue/components/courseware/CoursewareClipboardItem.vue
deleted file mode 100644
index aa975c8..0000000
--- a/resources/vue/components/courseware/CoursewareClipboardItem.vue
+++ /dev/null
@@ -1,246 +0,0 @@
-<template>
-    <div class="cw-clipboard-item-wrapper">
-        <button class="cw-clipboard-item" :class="['cw-clipboard-item-' + kind]" @click.prevent="insertItem">
-            <header class="sr-only">
-                {{ srTitle }}
-            </header>
-            <header class="cw-clipboard-item-title" aria-hidden="true">
-                {{ name }}
-            </header>
-            <p class="cw-clipboard-item-description">
-                {{ description }}
-            </p>
-        </button>
-        <div class="cw-clipboard-item-action-menu-wrapper">
-            <studip-action-menu
-                class="cw-clipboard-item-action-menu"
-                :items="menuItems"
-                :context="name"
-                @insertItemCopy="insertItemCopy"
-                @editItem="showEditItem"
-                @deleteItem="deleteItem"
-            />
-        </div>
-        <studip-dialog
-            v-if="showEditDialog"
-            :title="$gettext('Umbenennen')"
-            :confirmText="$gettext('Speichern')"
-            confirmClass="accept"
-            :closeText="$gettext('Abbrechen')"
-            closeClass="cancel"
-            height="360"
-            width="500"
-            @close="closeEditItem"
-            @confirm="storeItem"
-        >
-            <template v-slot:dialogContent>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Titel') }}
-                        <input type="text" v-model="currentClipboard.attributes.name" />
-                    </label>
-                    <label>
-                        {{ $gettext('Beschreibung') }}
-                        <textarea v-model="currentClipboard.attributes.description"></textarea>
-                    </label>
-                </form>
-            </template>
-        </studip-dialog>
-    </div>
-</template>
-
-<script>
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-clipboard-item',
-    components: {},
-    props: {
-        clipboard: Object,
-    },
-    data() {
-        return {
-            showEditDialog: false,
-            currentClipboard: null,
-
-            text: {
-                errorMessage: this.$gettext('Es ist ein Fehler aufgetreten.'),
-                positionWarning: this.$gettext(
-                    'Bitte wählen Sie einen Ort aus, an dem der Block eingefügt werden soll.'
-                ),
-                blockSuccess: this.$gettext('Der Block wurde erfolgreich eingefügt.'),
-                containerSuccess: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'),
-            },
-        };
-    },
-    computed: {
-        ...mapGetters({
-            blockAdder: 'blockAdder',
-            currentElement: 'currentElement',
-        }),
-        name() {
-            return this.clipboard.attributes.name;
-        },
-        description() {
-            return this.clipboard.attributes.description;
-        },
-        isBlock() {
-            return this.clipboard.attributes['object-type'] === 'courseware-blocks';
-        },
-        kind() {
-            return this.clipboard.attributes['object-kind'];
-        },
-        blockId() {
-            return this.clipboard.attributes['block-id'];
-        },
-        blockNotFound() {
-            return this.clipboard.relationships.block.data === null;
-        },
-        containerId() {
-            return this.clipboard.attributes['container-id'];
-        },
-        containerNotFound() {
-            return this.clipboard.relationships.container.data === null;
-        },
-        itemNotFound() {
-            if (this.isBlock) {
-                return this.blockNotFound;
-            }
-
-            return this.containerNotFound;
-        },
-        menuItems() {
-            let menuItems = [];
-            if (!this.itemNotFound) {
-                menuItems.push({
-                    id: 1,
-                    label: this.$gettext('Kopie des aktuellen Stands einfügen'),
-                    icon: 'copy',
-                    emit: 'insertItemCopy',
-                });
-            }
-            menuItems.push({ id: 2, label: this.$gettext('Umbenennen'), icon: 'edit', emit: 'editItem' });
-            menuItems.push({ id: 3, label: this.$gettext('Löschen'), icon: 'trash', emit: 'deleteItem' });
-
-            menuItems.sort((a, b) => a.id - b.id);
-            return menuItems;
-        },
-        blockAdderActive() {
-            return Object.keys(this.blockAdder).length !== 0;
-        },
-        srTitle() {
-            return this.isBlock ? 
-                this.$gettextInterpolate(this.$gettext(`Block %{name} einfügen`), { name: this.name }) :
-                this.$gettextInterpolate(this.$gettext(`Abschnitt %{name} einfügen`), { name: this.name });
-        }
-    },
-    methods: {
-        ...mapActions({
-            companionInfo: 'companionInfo',
-            companionSuccess: 'companionSuccess',
-            companionWarning: 'companionWarning',
-            copyContainer: 'copyContainer',
-            copyBlock: 'copyBlock',
-            clipboardInsertBlock: 'clipboardInsertBlock',
-            clipboardInsertContainer: 'clipboardInsertContainer',
-            loadStructuralElement: 'loadStructuralElement',
-            loadContainer: 'loadContainer',
-            deleteClipboard: 'courseware-clipboards/delete',
-            updateClipboard: 'courseware-clipboards/update',
-            loadClipboard: 'courseware-clipboards/loadById',
-        }),
-
-        async insertItem() {
-            let insertError = false;
-
-            if (this.isBlock) {
-                if (!this.blockAdderActive) {
-                    this.companionWarning({ info: this.text.positionWarning });
-                    return;
-                }
-                try {
-                    await this.clipboardInsertBlock({
-                        parentId: this.blockAdder.container.id,
-                        section: this.blockAdder.section,
-                        clipboard: this.clipboard,
-                    });
-                } catch (error) {
-                    insertError = true;
-                    this.companionWarning({ info: this.text.errorMessage });
-                }
-                if (!insertError) {
-                    await this.loadContainer(this.blockAdder.container.id);
-                    this.companionSuccess({ info: this.text.blockSuccess });
-                }
-            } else {
-                try {
-                    await this.clipboardInsertContainer({
-                        parentId: this.currentElement,
-                        clipboard: this.clipboard,
-                    });
-                } catch (error) {
-                    insertError = true;
-                    this.companionWarning({ info: this.text.errorMessage });
-                }
-                if (!insertError) {
-                    this.loadStructuralElement(this.currentElement);
-                    this.companionSuccess({ info: this.text.containerSuccess });
-                }
-            }
-        },
-
-        async insertItemCopy() {
-            let insertError = false;
-
-            if (this.isBlock) {
-                if (!this.blockAdderActive) {
-                    this.companionWarning({ info: this.text.positionWarning });
-                    return;
-                }
-                try {
-                    await this.copyBlock({
-                        parentId: this.blockAdder.container.id,
-                        section: this.blockAdder.section,
-                        block: { id: this.blockId },
-                    });
-                } catch (error) {
-                    insertError = true;
-                    this.companionWarning({ info: this.text.errorMessage });
-                }
-                if (!insertError) {
-                    await this.loadContainer(this.blockAdder.container.id);
-                    this.companionSuccess({ info: this.text.blockSuccess });
-                }
-            } else {
-                try {
-                    await this.copyContainer({ parentId: this.currentElement, container: { id: this.containerId } });
-                } catch (error) {
-                    insertError = true;
-                    this.companionWarning({ info: this.text.errorMessage });
-                }
-                if (!insertError) {
-                    this.loadStructuralElement(this.currentElement);
-                    this.companionSuccess({ info: this.text.containerSuccess });
-                }
-            }
-        },
-        deleteItem() {
-            this.deleteClipboard({ id: this.clipboard.id });
-        },
-        showEditItem() {
-            this.showEditDialog = true;
-        },
-        closeEditItem() {
-            this.showEditDialog = false;
-        },
-        async storeItem() {
-            this.closeEditItem();
-            await this.updateClipboard(this.currentClipboard);
-            this.loadClipboard({ id: this.currentClipboard.id });
-        },
-    },
-    mounted() {
-        this.currentClipboard = _.cloneDeep(this.clipboard);
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareCodeBlock.vue b/resources/vue/components/courseware/CoursewareCodeBlock.vue
deleted file mode 100644
index 3ed8d22..0000000
--- a/resources/vue/components/courseware/CoursewareCodeBlock.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<template>
-    <div class="cw-block cw-block-code">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <pre v-show="currentContent !== ''"  v-highlightjs="currentContent"><code ref="code" :class="[currentLang]"></code></pre>
-                <div v-show="currentLang !== ''" class="code-lang">
-                    <span>{{ currentLang }}</span>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Sprache') }}
-                        <input type="text" v-model="currentLang" />
-                    </label>
-                    <label>
-                        {{ $gettext('Quelltext') }}
-                        <textarea v-model="currentContent"></textarea>
-                    </label>
-                </form>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Quelltext-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { blockMixin } from './block-mixin.js';
-import hljs from 'highlight.js';
-
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-code-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentLang: '',
-            currentContent: '',
-        };
-    },
-    computed: {
-        content() {
-            return this.block?.attributes?.payload?.content;
-        },
-        lang() {
-            return this.block?.attributes?.payload?.lang;
-        },
-    },
-    directives: {
-        highlightjs: {
-            deep: true,
-            bind(el, binding) {
-                let targets = el.querySelectorAll('code');
-                targets.forEach((target) => {
-                    if (binding.value) {
-                        target.innerHTML = binding.value;
-                    }
-                    hljs.highlightBlock(target);
-                });
-            },
-            componentUpdated(el, binding) {
-                let targets = el.querySelectorAll('code');
-                targets.forEach((target) => {
-                    if (binding.value) {
-                        target.innerHTML = binding.value;
-                        hljs.highlightBlock(target);
-                    }
-                });
-            },
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            this.currentLang = this.lang;
-            this.currentContent = this.content;
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.lang = this.currentLang;
-            attributes.payload.content = this.currentContent;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareCollapsibleBox.vue b/resources/vue/components/courseware/CoursewareCollapsibleBox.vue
deleted file mode 100644
index c4f9233..0000000
--- a/resources/vue/components/courseware/CoursewareCollapsibleBox.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<template>
-    <div class="cw-collapsible" :class="{ 'cw-collapsible-open': isOpen }">
-        <a href="#" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen">
-            <header :class="{ 'cw-collapsible-open': isOpen }" class="cw-collapsible-title">
-                <studip-icon v-if="icon" :shape="icon" /> {{ title }}
-            </header>
-        </a>
-        <div class="cw-collapsible-content" :class="{ 'cw-collapsible-content-open': isOpen }">
-            <slot></slot>
-        </div>
-    </div>
-</template>
-
-<script>
-import StudipIcon from './../StudipIcon.vue';
-
-export default {
-    name: 'courseware-collapsible-box',
-    components: {
-        StudipIcon,
-    },
-    props: {
-        title: String,
-        icon: {
-            type: String,
-            default: '',
-        },
-        open: {
-            type: Boolean,
-            default: false,
-        },
-    },
-    data() {
-        return {
-            isOpen: this.open,
-        };
-    },
-    mounted(){
-        this.updateCollapsible();
-    },
-    updated() {
-        this.updateCollapsible();
-    },
-    methods: {
-        updateCollapsible() {
-            if (this.isOpen) {
-                STUDIP.eventBus.emit('courseware:update-collapsible', { 'uid': this._uid });
-            }
-        }
-    },
-    watch: {
-        open(state) {
-            this.isOpen = state;
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareCommentsOverviewDialog.vue b/resources/vue/components/courseware/CoursewareCommentsOverviewDialog.vue
index 3858479..825ee8f 100644
--- a/resources/vue/components/courseware/CoursewareCommentsOverviewDialog.vue
+++ b/resources/vue/components/courseware/CoursewareCommentsOverviewDialog.vue
@@ -31,17 +31,17 @@
 </template>
 
 <script>
-import CoursewareBlockComments from './CoursewareBlockComments.vue';
-import CoursewareStructuralElementComments from './CoursewareStructuralElementComments.vue';
-import CoursewareBlockFeedback from './CoursewareBlockFeedback.vue';
-import CoursewareStructuralElementFeedback from './CoursewareStructuralElementFeedback.vue';
+import CoursewareBlockComments from './blocks/CoursewareBlockComments.vue';
+import CoursewareBlockFeedback from './blocks/CoursewareBlockFeedback.vue';
+import CoursewareStructuralElementComments from './structural-element/CoursewareStructuralElementComments.vue';
+import CoursewareStructuralElementFeedback from './structural-element/CoursewareStructuralElementFeedback.vue';
 
 export default {
     name: 'courseware-comments-overview-dialog',
     components: {
         CoursewareBlockComments,
-        CoursewareStructuralElementComments,
         CoursewareBlockFeedback,
+        CoursewareStructuralElementComments,
         CoursewareStructuralElementFeedback
     },
     props: {
diff --git a/resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterCreated.vue b/resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterCreated.vue
deleted file mode 100644
index 96074cd..0000000
--- a/resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterCreated.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<template>
-    <sidebar-widget :title="$gettext('Neueste Einträge')">
-        <template #content>
-            <div class="cw-filter-widget">
-                <form class="default" @submit.prevent="">
-                    <select v-model="createdFilter" :aria-label="$gettext('Filter: Neueste Einträge')">
-                        <option value="all">
-                            {{ $gettext('unbeschränkt') }}
-                        </option>
-                        <option value="oneDay">
-                            {{ $gettext('einen Tag alt') }}
-                        </option>
-                        <option value="oneWeek">
-                            {{ $gettext('eine Woche alt') }}
-                        </option>
-                    </select>
-                </form>
-            </div>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-comments-overview-widget-filter-created',
-    components: {
-        SidebarWidget
-    },
-    data() {
-        return {
-            createdFilter: 'all',
-        };
-    },
-    methods: {
-        ...mapActions({
-            setCreatedFilter: 'setCreatedFilter',
-        }),
-        filterCreated() {
-            this.setCreatedFilter(this.createdFilter);
-        },
-    },
-    watch: {
-        createdFilter() {
-            this.filterCreated();
-        },
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterType.vue b/resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterType.vue
deleted file mode 100644
index 9aaf617..0000000
--- a/resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterType.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<template>
-    <sidebar-widget :title="$gettext('Listen')">
-        <template #content>
-            <div class="cw-filter-widget">
-                <form class="default" @submit.prevent="">
-                    <select v-model="typeFilter" :aria-label="$gettext('Filter: Listen')">
-                        <option value="all">
-                            {{ $gettext('Blöcke und Seiten') }}
-                        </option>
-                        <option value="blocks">
-                            {{ $gettext('Blöcke') }}
-                        </option>
-                        <option value="elements">
-                            {{ $gettext('Seiten') }}
-                        </option>
-                    </select>
-                </form>
-            </div>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-comments-overview-widget-filter-type',
-    components: {
-        SidebarWidget
-    },
-    data() {
-        return {
-            typeFilter: 'all',
-        };
-    },
-    methods: {
-        ...mapActions({
-            setTypeFilter: 'setTypeFilter',
-        }),
-        filterType() {
-            this.setTypeFilter(this.typeFilter);
-        },
-    },
-    watch: {
-        typeFilter() {
-            this.filterType();
-        },
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterUnit.vue b/resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterUnit.vue
deleted file mode 100644
index 4d0afd1..0000000
--- a/resources/vue/components/courseware/CoursewareCommentsOverviewWidgetFilterUnit.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-        <sidebar-widget :title="$gettext('Lernmaterial')">
-        <template #content>
-            <div class="cw-filter-widget">
-                <form class="default" @submit.prevent="">
-                    <select v-model="unitFilter" :aria-label="$gettext('Filter: Lernmaterial')">
-                        <option value="all">
-                            {{ $gettext('Alle') }}
-                        </option>
-                        <option v-for="unit in sortedUnits" :key="unit.id" :value="unit.id">
-                            {{ getUnitName(unit) }}
-                        </option>
-                    </select>
-                </form>
-            </div>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-comments-overview-widget-filter-unit',
-    data() {
-        return {
-            unitFilter: 'all',
-        };
-    },
-    computed: {
-        ...mapGetters({
-            units: 'courseware-units/all',
-            elementById: 'courseware-structural-elements/byId',
-        }),
-        sortedUnits() {
-            let units = _.cloneDeep(this.units);
-            units = units.sort((a, b) => this.getUnitName(a) < this.getUnitName(b) ? -1 : 1);
-
-            return units;
-        }
-    },
-    methods: {
-        ...mapActions({
-            setUnitFilter: 'setUnitFilter',
-        }),
-        filterUnit() {
-            this.setUnitFilter(this.unitFilter);
-        },
-        getUnitName(unit) {
-            return this.elementById({ id: unit.relationships['structural-element'].data.id}).attributes.title;
-        }
-    },
-    watch: {
-        unitFilter() {
-            this.filterUnit();
-        },
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareCompanionBox.vue b/resources/vue/components/courseware/CoursewareCompanionBox.vue
deleted file mode 100644
index 2e82695..0000000
--- a/resources/vue/components/courseware/CoursewareCompanionBox.vue
+++ /dev/null
@@ -1,24 +0,0 @@
-<template>
-    <div class="cw-companion-box" :class="[mood]">
-        <div>
-            <p v-html="msgCompanion"></p>
-            <slot name="companionActions"></slot>
-        </div>
-    </div>
-</template>
-
-<script>
-export default {
-    name: 'courseware-companion-box',
-    props: {
-        msgCompanion: String,
-        mood: {
-            type: String,
-            default: 'default',
-            validator: value => {
-                return ['default','unsure', 'special', 'sad', 'pointing', 'curious'].includes(value);
-            }
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareCompanionOverlay.vue b/resources/vue/components/courseware/CoursewareCompanionOverlay.vue
deleted file mode 100644
index 9cce904..0000000
--- a/resources/vue/components/courseware/CoursewareCompanionOverlay.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-    <div
-        class="cw-companion-overlay"
-        :class="[showCompanion ? 'cw-companion-overlay-in' : '', showCompanion ? '' : 'cw-companion-overlay-out', styleCompanion]"
-    >
-        <div class="cw-companion-overlay-content" v-html="msgCompanion"></div>
-        <button class="cw-compantion-overlay-close" @click="hideCompanion"></button>
-    </div>
-</template>
-
-<script>
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-companion-overlay',
-    computed: {
-        ...mapGetters({
-            showCompanion: 'showCompanionOverlay',
-            msgCompanion: 'msgCompanionOverlay',
-            styleCompanion: 'styleCompanionOverlay',
-            showToolbar: 'showToolbar',
-        }),
-    },
-    methods: {
-        ...mapActions({
-            coursewareShowCompanionOverlay: 'coursewareShowCompanionOverlay'
-        }),
-        hideCompanion() {
-            this.coursewareShowCompanionOverlay(false);
-        },
-    },
-    watch: {
-        showCompanion(newValue, oldValue) {
-            let view = this;
-            if (newValue === true && oldValue === false) {
-                setTimeout(() => {
-                    view.hideCompanion();
-                }, 4000);
-            }
-        },
-        showToolbar(newValue, oldValue) {
-            // hide companion when toolbar is closed 
-            if (oldValue === true && newValue === false) {
-                this.hideCompanion();
-            }
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareConfirmBlock.vue b/resources/vue/components/courseware/CoursewareConfirmBlock.vue
deleted file mode 100644
index 58dbc68..0000000
--- a/resources/vue/components/courseware/CoursewareConfirmBlock.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<template>
-    <div class="cw-block cw-block-confirm">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            :defaultGrade="false"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div class="cw-block-title">
-                    {{ $gettext('Bestätigung') }}
-                </div>
-                <form class="default cw-block-confirm-content" prevent.default="">
-                    <label>
-                        <input type="checkbox" :disabled="confirm" :checked="confirm" @click="setConfirm"/>
-                        <span>{{ currentText }}</span>
-                    </label>
-                </form>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Text') }}
-                        <input type="text" v-model="currentText" />
-                    </label>
-                </form>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Bestätigungs-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { mapActions, mapGetters } from 'vuex';
-import { blockMixin } from './block-mixin.js';
-
-export default {
-    name: 'courseware-confirm-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentText: '',
-            confirm: false,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            getUserDataById: 'courseware-user-data-fields/byId',
-        }),
-        text() {
-            return this.block?.attributes?.payload?.text;
-        },
-        userData() {
-            return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id });
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            updateUserDataFields: 'courseware-user-data-fields/update'
-        }),
-        initCurrentData() {
-            this.currentText = this.text;
-            if (this.userData?.attributes?.payload?.confirm) {
-                this.confirm = this.userData.attributes.payload.confirm;
-            }
-        },
-        async setConfirm() {
-            if (this.confirm) {
-                return;
-            }
-            let data = {};
-            data.type = 'courseware-user-data-fields';
-            data.id = this.block.relationships['user-data-field'].data.id;
-            data.attributes = {};
-            data.attributes.payload = {};
-            data.attributes.payload.confirm = true;
-            data.relationships = {};
-            data.relationships.block = {};
-            data.relationships.block.data = {};
-            data.relationships.block.data.id = this.block.id;
-            data.relationships.block.data.type = this.block.type;
-
-            await this.updateUserDataFields(data);
-            this.userProgress = 1;
-            this.confirm = true;
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.text = this.currentText;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareContainerActions.vue b/resources/vue/components/courseware/CoursewareContainerActions.vue
deleted file mode 100644
index 2480aba..0000000
--- a/resources/vue/components/courseware/CoursewareContainerActions.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<template>
-    <div v-if="canEdit" class="cw-container-actions">
-        <studip-action-menu
-            :items="menuItems"
-            :context="container.attributes.title"
-            @editContainer="editContainer"
-            @changeContainer="changeContainer"
-            @deleteContainer="deleteContainer"
-            @removeLock="removeLock"
-            @copyToClipboard="copyToClipboard"
-        />
-    </div>
-</template>
-
-<script>
-import { mapGetters } from 'vuex';
-export default {
-    name: 'courseware-container-actions',
-    props: {
-        canEdit: Boolean,
-        container: Object,
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            userIsTeacher: 'userIsTeacher',
-        }),
-        blocked() {
-            return this.container?.relationships?.['edit-blocker']?.data !== null;
-        },
-        blockerId() {
-            return this.blocked ? this.container?.relationships?.['edit-blocker']?.data?.id : null;
-        },
-        blockedByThisUser() {
-            return this.blocked && this.userId === this.blockerId;
-        },
-        blockedByAnotherUser() {
-            return this.blocked && this.userId !== this.blockerId;
-        },
-        menuItems() {
-            let menuItems = [];
-            if (!this.blockedByAnotherUser) {
-                if (this.container.attributes["container-type"] !== 'list') {
-                    menuItems.push({ id: 1, label: this.$gettext('Abschnitt bearbeiten'), icon: 'edit', emit: 'editContainer' });
-                }
-                menuItems.push({ id: 2, label: this.$gettext('Abschnitt verändern'), icon: 'settings', emit: 'changeContainer' });
-                menuItems.push({ id: 3, label: this.$gettext('Abschnitt merken'), icon: 'clipboard', emit: 'copyToClipboard' });
-                menuItems.push({ id: 5, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' });
-            }
-
-            if (this.blocked && this.blockedByAnotherUser && this.userIsTeacher) {
-                menuItems.push({
-                    id: 3,
-                    label: this.$gettext('Sperre aufheben'),
-                    icon: 'lock-unlocked',
-                    emit: 'removeLock',
-                });
-            }
-
-            menuItems.sort((a, b) => {
-                return a.id > b.id ? 1 : b.id > a.id ? -1 : 0;
-            });
-            return menuItems;
-        },
-
-    },
-    methods: {
-        menuAction(action) {
-            this[action]();
-        },
-        editContainer() {
-            this.$emit('editContainer');
-        },
-        changeContainer() {
-            this.$emit('changeContainer');
-        },
-        deleteContainer() {
-            this.$emit('deleteContainer');
-        },
-        removeLock() {
-            this.$emit('removeLock');
-        },
-        copyToClipboard() {
-            this.$emit('copyToClipboard');
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareContainerAdderItem.vue b/resources/vue/components/courseware/CoursewareContainerAdderItem.vue
deleted file mode 100644
index 78cc4ed..0000000
--- a/resources/vue/components/courseware/CoursewareContainerAdderItem.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<template>
-    <a href="#" @click.prevent="addContainer">
-        <div class="cw-containeradder-item" :class="['cw-containeradder-item-' + type]">
-            <header class="cw-containeradder-item-title">
-                {{ title }}
-            </header>
-            <p class="cw-containeradder-item-description">
-                {{ description }}
-            </p>
-        </div>
-    </a>
-</template>
-<script>
-import { mapActions } from 'vuex';
-export default {
-    name: 'courseware-container-adder-item',
-    components: {},
-    props: {
-        title: String,
-        description: String,
-        type: String,
-        colspan: String,
-        firstSection: String,
-        secondSection: String,
-    },
-    methods: {
-        ...mapActions({
-            createContainer: 'createContainer',
-            companionSuccess: 'companionSuccess',
-        }),
-        async addContainer() {
-            let attributes = {};
-            attributes["container-type"] = this.type;
-            let sections = [];
-            if (this.type === 'list') {
-                sections = [{ name: this.firstSection, icon: '', blocks: [] }];
-            } else {
-                sections = [{ name: this.firstSection, icon: '', blocks: [] },{ name: this.secondSection, icon: '', blocks: [] }];
-            }
-            attributes.payload = {
-                colspan: this.colspan,
-                sections: sections,
-            };
-            await this.createContainer({ structuralElementId: this.$route.params.id, attributes: attributes });
-            this.companionSuccess({
-                info: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'),
-            });
-        },
-    },
-    mounted() {},
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareDashboardStudents.vue b/resources/vue/components/courseware/CoursewareDashboardStudents.vue
index 897388d..ff59669 100644
--- a/resources/vue/components/courseware/CoursewareDashboardStudents.vue
+++ b/resources/vue/components/courseware/CoursewareDashboardStudents.vue
@@ -233,8 +233,8 @@
 <script>
 import StudipIcon from './../StudipIcon.vue';
 import StudipDialog from './../StudipDialog.vue';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareDateInput from './CoursewareDateInput.vue';
+import CoursewareCompanionBox from './layouts/CoursewareCompanionBox.vue';
+import CoursewareDateInput from './layouts/CoursewareDateInput.vue';
 import CoursewareTasksDialogDistribute from './CoursewareTasksDialogDistribute.vue';
 import taskHelperMixin from '../../mixins/courseware/task-helper.js';
 import { mapActions, mapGetters } from 'vuex';
diff --git a/resources/vue/components/courseware/CoursewareDashboardTasks.vue b/resources/vue/components/courseware/CoursewareDashboardTasks.vue
index 8bee6f5..caaea0b 100644
--- a/resources/vue/components/courseware/CoursewareDashboardTasks.vue
+++ b/resources/vue/components/courseware/CoursewareDashboardTasks.vue
@@ -102,10 +102,10 @@
     </div>
 </template>
 <script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import StudipIcon from './../StudipIcon.vue';
-import StudipActionMenu from './../StudipActionMenu.vue';
-import StudipDialog from './../StudipDialog.vue';
+import CoursewareCompanionBox from './layouts/CoursewareCompanionBox.vue';
+import StudipIcon from '../StudipIcon.vue';
+import StudipActionMenu from '../StudipActionMenu.vue';
+import StudipDialog from '../StudipDialog.vue';
 import taskHelperMixin from '../../mixins/courseware/task-helper.js';
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/CoursewareDateBlock.vue b/resources/vue/components/courseware/CoursewareDateBlock.vue
deleted file mode 100644
index 1361b60..0000000
--- a/resources/vue/components/courseware/CoursewareDateBlock.vue
+++ /dev/null
@@ -1,204 +0,0 @@
-<template>
-    <div class="cw-block cw-block-date">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="currentStyle === 'countdown'" class="cw-date-countdown">
-                    <div class="cw-date-countdown-digit" data-countdown="days">
-                        <div class="cw-date-countdown-number">{{ countdownDays }}</div>
-                        <div v-show="countdownDays === '01'" class="cw-date-countdown-label-sg">
-                            {{ $gettext('Tag') }}
-                        </div>
-                        <div v-show="countdownDays !== '01'" class="cw-date-countdown-label-pl">
-                            {{ $gettext('Tage') }}
-                        </div>
-                    </div>
-                    <div class="cw-date-countdown-digit" data-countdown="hours">
-                        <div class="cw-date-countdown-number">{{ countdownHours }}</div>
-                        <div v-show="countdownHours === '01'" class="cw-date-countdown-label-sg">
-                            {{ $gettext('Stunde') }}
-                        </div>
-                        <div v-show="countdownHours !== '01'" class="cw-date-countdown-label-pl">
-                            {{ $gettext('Stunden') }}
-                        </div>
-                    </div>
-                    <div class="cw-date-countdown-digit" data-countdown="minutes">
-                        <div class="cw-date-countdown-number">{{ countdownMinutes }}</div>
-                        <div v-show="countdownMinutes === '01'" class="cw-date-countdown-label-sg">
-                            {{ $gettext('Minute') }}
-                        </div>
-                        <div v-show="countdownMinutes !== '01'" class="cw-date-countdown-label-pl">
-                            {{ $gettext('Minuten') }}
-                        </div>
-                    </div>
-                    <div class="cw-date-countdown-digit" data-countdown="seconds">
-                        <div class="cw-date-countdown-number">{{ countdownSeconds }}</div>
-                        <div v-show="countdownSeconds === '01'" class="cw-date-countdown-label-sg">
-                            {{ $gettext('Sekunde') }}
-                        </div>
-                        <div v-show="countdownSeconds !== '01'" class="cw-date-countdown-label-pl">
-                            {{ $gettext('Sekunden') }}
-                        </div>
-                    </div>
-                </div>
-                <div v-if="currentStyle === 'date'" class="cw-date-date">
-                    <div class="cw-date-date-digits" data-date="date">
-                        <div class="cw-date-date-number">{{ currentDeDate }}</div>
-                    </div>
-                    <div class="cw-date-date-space"></div>
-                    <div class="cw-date-date-digits" data-date="time">
-                        <div class="cw-date-date-number">{{ currentTime }}</div>
-                    </div>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Datum') }}
-                        <input type="date" v-model="currentDate" @change="computeTimestamp" />
-                    </label>
-                    <label>
-                        {{ $gettext('Uhrzeit') }}
-                        <input type="time" v-model="currentTime" @change="computeTimestamp" />
-                    </label>
-                    <label>
-                        {{ $gettext('Layout') }}
-                        <select v-model="currentStyle">
-                            <option value="countdown">{{ $gettext('Countdown') }}</option>
-                            <option value="date">{{ $gettext('Datum') }}</option>
-                        </select>
-                    </label>
-                </form>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Termin-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { mapActions } from 'vuex';
-import { blockMixin } from './block-mixin.js';
-export default {
-    name: 'courseware-date-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentTimestamp: 0,
-            currentStyle: '',
-            currentTime: '',
-            currentDate: '',
-            currentDeDate: '',
-
-            countdownDays: '00',
-            countdownHours: '00',
-            countdownMinutes: '00',
-            countdownSeconds: '00',
-        };
-    },
-    computed: {
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        timestamp() {
-            return this.block?.attributes?.payload?.timestamp;
-        },
-        style() {
-            return this.block?.attributes?.payload?.style;
-        },
-        date() {
-            return new Date(this.currentTimestamp);
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-        if (this.currentStyle === 'countdown') {
-            this.countdown();
-        }
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            companionWarning: 'companionWarning',
-        }),
-        initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentTimestamp = this.timestamp;
-            this.currentDate =
-                this.date.getFullYear() +
-                '-' +
-                ('0' + (this.date.getMonth() + 1)).slice(-2) +
-                '-' +
-                ('0' + this.date.getDate()).slice(-2);
-            this.currentDeDate =
-                ('0' + this.date.getDate()).slice(-2) +
-                '.' +
-                ('0' + (this.date.getMonth() + 1)).slice(-2) +
-                '.' +
-                this.date.getFullYear();
-            this.currentTime = ('0' + this.date.getHours()).slice(-2) + ':' + ('0' + this.date.getMinutes()).slice(-2);
-            this.currentStyle = this.style;
-        },
-        countdown() {
-            let view = this;
-            setInterval(function () {
-                let now = new Date().getTime();
-                let distance = view.currentTimestamp - now;
-                if (distance < 0) {
-                    return;
-                }
-                view.countdownDays = Math.floor(distance / (1000 * 60 * 60 * 24));
-                view.countdownHours = ('0' + Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))).slice(
-                    -2
-                );
-                view.countdownMinutes = ('0' + Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60))).slice(-2);
-                view.countdownSeconds = ('0' + Math.floor((distance % (1000 * 60)) / 1000)).slice(-2);
-            }, 1000);
-        },
-        computeTimestamp() {
-            this.currentTimestamp = new Date(this.currentDate + ' ' + this.currentTime).getTime();
-        },
-        storeBlock() {
-            let cmpInfo = false;
-            if (this.currentDate === '') {
-                cmpInfo = this.$gettext('Bitte geben Sie ein Datum an.');
-            } else if (this.currentTime === '') {
-                cmpInfo = this.$gettext('Bitte geben Sie eine Uhrzeit an.');
-            }
-            if (cmpInfo) {
-                this.companionWarning({
-                    info: cmpInfo
-                });
-                return false;
-            } else {
-                let attributes = {};
-                attributes.payload = {};
-                attributes.payload.timestamp = this.currentTimestamp;
-                attributes.payload.style = this.currentStyle;
-
-                this.updateBlock({
-                    attributes: attributes,
-                    blockId: this.block.id,
-                    containerId: this.block.relationships.container.data.id,
-                });
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareDateInput.vue b/resources/vue/components/courseware/CoursewareDateInput.vue
deleted file mode 100644
index 57a5619..0000000
--- a/resources/vue/components/courseware/CoursewareDateInput.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
-    <input :value="formattedDate" @input="onInput" type="date" />
-</template>
-
-<script>
-const fromISO8601 = (string) => new Date(string);
-const toISO8601 = (date) => date.toISOString();
-const pad = (what, length = 2) => `00000000${what}`.substr(-length);
-
-export default {
-    props: ['value'],
-    data: () => ({
-        date: new Date(),
-    }),
-    computed: {
-        formattedDate() {
-            return `${this.date.getFullYear()}-${pad(this.date.getMonth() + 1)}-${pad(this.date.getDate())}`;
-        },
-    },
-    methods: {
-        onInput({ target }) {
-            const newValue = toISO8601(target.valueAsDate);
-            if (newValue !== this.value) {
-                this.$emit('input', newValue);
-            }
-        },
-    },
-    beforeMount() {
-        if (this.value) {
-            this.date = fromISO8601(this.value);
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/CoursewareDefaultBlock.vue
deleted file mode 100644
index a954860..0000000
--- a/resources/vue/components/courseware/CoursewareDefaultBlock.vue
+++ /dev/null
@@ -1,387 +0,0 @@
-<template>
-    <div v-if="block.attributes.visible || canEdit" class="cw-default-block">
-        <div class="cw-content-wrapper" :class="[showEditMode ? 'cw-content-wrapper-active' : '']">
-            <header v-if="showEditMode" class="cw-block-header">
-                <a href="#" class="cw-block-header-toggle" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen">
-                    <studip-icon :shape="isOpen ? 'arr_1down' : 'arr_1right'" />
-                    <span>{{ blockTitle }}</span>
-                    <studip-icon v-if="blockedByAnotherUser" shape="lock-locked" />
-                    <span v-if="blockedByAnotherUser" class="cw-default-block-blocker-warning">
-                        {{ $gettextInterpolate($gettext('Wird im Moment von %{ userName } bearbeitet'), { userName: this.blockingUserName }) }}
-                    </span>
-                    <studip-icon v-if="!block.attributes.visible" shape="visibility-invisible" />
-                    <span v-if="!block.attributes.visible" class="cw-default-block-invisible-info">
-                        {{ $gettext('Unsichtbar für Nutzende ohne Schreibrecht') }}
-                    </span>
-                </a>
-                <courseware-block-actions
-                    :block="block"
-                    :canEdit="canEdit"
-                    :deleteOnly="deleteOnly"
-                    @editBlock="displayFeature('Edit')"
-                    @showInfo="displayFeature('Info')"
-                    @showExportOptions="displayFeature('ExportOptions')"
-                    @deleteBlock="displayDeleteDialog()"
-                    @removeLock="displayRemoveLockDialog()"
-                    @copyToClipboard="copyToClipboard"
-                />
-            </header>
-            <div v-show="isOpen">
-                <div v-if="showContent" class="cw-block-content">
-                    <slot name="content" />
-                </div>
-                <div v-if="showFeatures" class="cw-block-features cw-block-features-default">
-                    <courseware-block-export-options
-                        v-if="canEdit && showExportOptions"
-                        :block="block"
-                        @close="displayFeature(false)"
-                    />
-                    <courseware-block-edit
-                        v-if="canEdit && showEdit"
-                        :block="block"
-                        :preview="preview"
-                        @store="prepareStoreEdit"
-                        @close="closeEdit"
-                    >
-                        <template #edit>
-                            <slot name="edit" />
-                        </template>
-                    </courseware-block-edit>
-                    <courseware-block-info v-if="showInfo" :block="block" @close="displayFeature(false)">
-                        <template #info>
-                            <slot name="info" />
-                        </template>
-                    </courseware-block-info>
-                </div>
-            </div>
-        </div>
-        <div v-if="discussView" class="cw-discuss-wrapper">
-            <courseware-block-discussion
-                :block="block"
-                :canEdit="canEdit"
-            />
-        </div>
-        <studip-dialog
-            v-if="showDeleteDialog"
-            :title="textDeleteTitle"
-            :question="textDeleteAlert"
-            height="180"
-            width="360"
-            @confirm="executeDelete"
-            @close="closeDeleteDialog"
-        ></studip-dialog>
-        <studip-dialog
-            v-if="showRemoveLockDialog"
-            :title="textRemoveLockTitle"
-            :question="textRemoveLockAlert"
-            height="200"
-            width="450"
-            @confirm="executeRemoveLock"
-            @close="showRemoveLockDialog = false"
-        ></studip-dialog>
-
-    </div>
-</template>
-
-<script>
-import CoursewareBlockEdit from './CoursewareBlockEdit.vue';
-import CoursewareBlockExportOptions from './CoursewareBlockExportOptions.vue';
-import CoursewareBlockInfo from './CoursewareBlockInfo.vue';
-import CoursewareBlockActions from './CoursewareBlockActions.vue';
-import StudipDialog from '../StudipDialog.vue';
-import StudipIcon from '../StudipIcon.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-import CoursewareBlockDiscussion from './CoursewareBlockDiscussion.vue';
-
-export default {
-    name: 'courseware-default-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareBlockEdit,
-        CoursewareBlockExportOptions,
-        CoursewareBlockActions,
-        CoursewareBlockInfo,
-        StudipDialog,
-        StudipIcon,
-        CoursewareBlockDiscussion,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        deleteOnly: {
-            type: Boolean,
-            default: false
-        },
-        isTeacher: Boolean,
-        preview: Boolean,
-        defaultGrade: {
-            type: Boolean,
-            default: true,
-        },
-    },
-    data() {
-        return {
-            showFeatures: false,
-            showExportOptions: false,
-            showEdit: false,
-            showInfo: false,
-            showContent: true,
-            showEditModeShortcut: false,
-            showDeleteDialog: false,
-            showRemoveLockDialog: false,
-            currentComments: [],
-            textDeleteTitle: this.$gettext('Block unwiderruflich löschen'),
-            textDeleteAlert: this.$gettext('Möchten Sie diesen Block wirklich löschen?'),
-            textRemoveLockTitle: this.$gettext('Sperre aufheben'),
-            textRemoveLockAlert: this.$gettext('Möchten Sie die Sperre dieses Blocks wirklich aufheben?'),
-            isOpen: true,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            blockTypes: 'blockTypes',
-            containerById: 'courseware-containers/byId',
-            context: 'context',
-            userId: 'userId',
-            userById: 'users/byId',
-            viewMode: 'viewMode',
-            currentElementisLink: 'currentElementisLink'
-        }),
-        showEditMode() {
-            let show = (this.viewMode === 'edit' || this.blockedByThisUser) && !this.currentElementisLink;
-            if (!show) {
-                this.displayFeature(false);
-            }
-            return show;
-        },
-        discussView() {
-            return this.viewMode === 'discuss';
-        },
-        blocked() {
-            return this.block?.relationships?.['edit-blocker']?.data !== null;
-        },
-        blockerId() {
-            return this.blocked ? this.block?.relationships?.['edit-blocker']?.data?.id : null;
-        },
-        blockedByThisUser() {
-            return this.blocked && this.userId === this.blockerId;
-        },
-        blockedByAnotherUser() {
-            return this.blocked && this.userId !== this.blockerId;
-        },
-        blockingUser() {
-            if (this.blockedByAnotherUser) {
-                return this.userById({id: this.blockerId});
-            }
-
-            return null;
-        },
-        blockingUserName() {
-            return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : '';
-        },
-        blockTitle() {
-            const type = this.block.attributes['block-type'];
-
-            return this.blockTypes.find((blockType) => blockType.type === type)?.title || this.$gettext('Fehler');
-        },
-        public() {
-            return this.context.type === 'public';
-        }
-    },
-    mounted() {
-        if (this.blocked) {
-            if (this.blockedByThisUser) {
-                this.displayFeature('Edit');
-            }
-        }
-        if (!this.public && this.userProgress && this.userProgress.attributes.grade === 0 && this.defaultGrade) {
-            this.userProgress = 1;
-        }
-    },
-    methods: {
-        ...mapActions({
-            companionInfo: 'companionInfo',
-            companionWarning: 'companionWarning',
-            companionSuccess: 'companionSuccess',
-            deleteBlock: 'deleteBlockInContainer',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-            loadContainer: 'loadContainer',
-            loadBlock: 'courseware-blocks/loadById',
-            updateContainer: 'updateContainer',
-            createClipboard: 'courseware-clipboards/create'
-        }),
-        async displayFeature(element) {
-            if (this.showEdit && element === 'Edit') {
-                return false;
-            }
-            this.showFeatures = false;
-            this.showExportOptions = false;
-            this.showEdit = false;
-            this.showInfo = false;
-            this.showContent = true;
-            if (element) {
-                if (element === 'Edit') {
-                    await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
-                    if (!this.blocked) {
-                        await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
-                        if (!this.preview) {
-                            this.showContent = false;
-                        }
-                        this['show' + element] = true;
-                        this.showFeatures = true;
-                    } else {
-                        if (this.blockedByThisUser) {
-                            if (!this.preview) {
-                                this.showContent = false;
-                            }
-                            this['show' + element] = true;
-                            this.showFeatures = true;
-                        } else {
-                            this.companionInfo({ info: this.$gettext('Dieser Block wird bereits bearbeitet.') });
-                        }
-                    }
-                } else {
-                    this['show' + element] = true;
-                    this.showFeatures = true;
-                }
-            }
-        },
-        prepareStoreEdit() {
-            // storeEdit is only emitted when the block is not in deleting process.
-            if (!this.showDeleteDialog) {
-                this.storeBlock();
-            }
-        },
-        async storeBlock() {
-            await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
-
-            if (this.blockedByThisUser) {
-                this.$emit('storeEdit');
-            }
-
-            if (this.blockedByAnotherUser) {
-                this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
-                    )
-                });
-                this.displayFeature(false);
-                this.$emit('closeEdit');
-            }
-            if (this.blockerId === null) {
-                await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
-                this.$emit('storeEdit');
-            }
-        },
-        async closeEdit() {
-            await this.loadBlock({ id: this.block.id , options: { include: 'edit-blocker' } }); // has block editor lock changed?
-            this.displayFeature(false);
-            this.$emit('closeEdit');
-            if (this.blockedByThisUser) {
-                await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' });
-            }
-            this.loadBlock({ id: this.block.id , options: { include: 'edit-blocker' } }); // to update block editor lock
-        },
-        async displayDeleteDialog() {
-            await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
-            if (!this.blocked) {
-                await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
-                this.showDeleteDialog = true;
-            } else {
-                if (this.blockedByThisUser) {
-                    this.showDeleteDialog = true;
-                } else {
-                    this.companionInfo({
-                        info: this.$gettextInterpolate(
-                            this.$gettext('Löschen nicht möglich, da %{blockingUserName} den Block bearbeitet.'),
-                            {blockingUserName: this.blockingUserName}
-                        )
-                    });
-                }
-            }
-        },
-        async closeDeleteDialog() {
-            await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
-            if (this.blockedByThisUser) {
-                await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' });
-            }
-            this.showDeleteDialog = false;
-        },
-        async executeDelete() {
-            this.showDeleteDialog = false;
-            this.displayFeature(false);
-            this.$emit('closeEdit');
-            await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
-            if (this.blockedByAnotherUser) {
-                this.companionInfo({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
-                    )
-                });
-                return false;
-            }
-            const containerId = this.block.relationships.container.data.id;
-            await this.loadContainer(containerId);
-            let container = this.containerById({id: containerId});
-            const structuralElementId = container.relationships['structural-element'].data.id;
-            let containerBlocks = container.relationships.blocks.data.map(block => {
-                return block.id;
-            });
-            let sections = container.attributes.payload.sections;
-
-            // lock parent container
-            await this.lockObject({ id: containerId, type: 'courseware-containers' });
-            // update container information
-            for (let i = 0; i < sections.length; i++) {
-                for (let j = 0; j < sections[i].blocks.length; j++) {
-                    let blockId = sections[i].blocks[j];
-                    if (!containerBlocks.includes(blockId) || blockId === this.block.id) {
-                        sections[i].blocks.splice(j, 1);
-                        j--;
-                    }
-                }
-            }
-            // update container
-            await this.updateContainer({ container, structuralElementId });
-            // unlock container
-            await this.unlockObject({ id: containerId, type: 'courseware-containers' });
-            await this.loadContainer(containerId);
-            this.deleteBlock({
-                blockId: this.block.id,
-                containerId: containerId,
-            });
-        },
-        displayRemoveLockDialog() {
-            this.showRemoveLockDialog = true;
-        },
-        async executeRemoveLock() {
-            await this.unlockObject({ id: this.block.id , type: 'courseware-blocks' });
-            await this.loadBlock({ id: this.block.id });
-            this.showRemoveLockDialog = false;
-        },
-        async copyToClipboard() {
-            const clipboard = {
-                attributes: {
-                    name: this.block.attributes.title,
-                    'block-id': this.block.id,
-                    'object-type': this.block.type,
-                    'object-kind': this.block.attributes['block-type'],
-                }
-            };
-
-            await this.createClipboard(clipboard, { root: true });
-            this.companionSuccess({ info: this.$gettext('Block wurde in Merkliste abgelegt.') });
-        }
-
-    },
-    watch: {
-        showEdit(state) {
-            this.$emit('showEdit', state);
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareDefaultBlockElements.vue b/resources/vue/components/courseware/CoursewareDefaultBlockElements.vue
deleted file mode 100644
index 17d8834..0000000
--- a/resources/vue/components/courseware/CoursewareDefaultBlockElements.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-    <div class="cw-default-block-elements">
-        <courseware-block-actions
-            :block="block"
-            :canEdit="canEdit"
-            @editBlock="editBlock"
-            @showExportOptions="showExportOptions"
-        />
-        <courseware-block-feedback v-if="canEdit" :block="block" :canEdit="canEdit" :isTeacher="isTeacher" />
-        <courseware-block-comments :block="block" />
-        <courseware-block-export-options v-if="canEdit" :block="block" />
-    </div>
-</template>
-
-<script>
-import CoursewareBlockActions from './CoursewareBlockActions.vue';
-import CoursewareBlockComments from './CoursewareBlockComments.vue';
-import CoursewareBlockExportOptions from './CoursewareBlockExportOptions.vue';
-import CoursewareBlockFeedback from './CoursewareBlockFeedback.vue';
-
-export default {
-    name: 'courseware-default-block-elements',
-    components: {
-        CoursewareBlockActions,
-        CoursewareBlockComments,
-        CoursewareBlockExportOptions,
-        CoursewareBlockFeedback,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-        showFeatures: Boolean,
-    },
-    data() {
-        return {
-            displayFeatures: false,
-        };
-    },
-    methods: {
-        editBlock() {
-            this.$emit('editBlock');
-        },
-        showExportOptions() {},
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/CoursewareDefaultContainer.vue
deleted file mode 100644
index 7e486d9..0000000
--- a/resources/vue/components/courseware/CoursewareDefaultContainer.vue
+++ /dev/null
@@ -1,392 +0,0 @@
-<template>
-    <div
-        class="cw-container cw-container-list"
-        :class="['cw-container-colspan-' + colSpan, showEditMode && canEdit ? 'cw-container-active' : '']"
-    >
-        <div class="cw-container-content">
-            <header v-if="showEditMode && canEdit" class="cw-container-header" :class="{ 'cw-container-header-open': isOpen }">
-                <a href="#" class="cw-container-header-toggle" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen">
-                    <studip-icon :shape="isOpen ? 'arr_1down' : 'arr_1right'" />
-                    <span>{{ container.attributes.title }} ({{container.attributes.width}})</span>
-                    <studip-icon v-if="blockedByAnotherUser" shape="lock-locked" />
-                    <span v-if="blockedByAnotherUser" class="cw-default-container-blocker-warning">
-                        {{ $gettextInterpolate($gettext('Wird im Moment von %{ userName } bearbeitet'), { userName: this.blockingUserName }) }}
-                    </span>
-                </a>
-                <courseware-container-actions
-                    :canEdit="canEdit"
-                    :container="container"
-                    @editContainer="displayEditDialog"
-                    @changeContainer="displayChangeDialog"
-                    @deleteContainer="displayDeleteDialog"
-                    @removeLock="displayRemoveLockDialog"
-                    @copyToClipboard="copyToClipboard"
-                />
-            </header>
-            <div v-show="isOpen"
-                class="cw-block-wrapper"
-                :class="{
-                    'cw-block-wrapper-active': showEditMode,
-                }"
-            >
-                <slot name="containerContent"></slot>
-            </div>
-
-            <studip-dialog
-                v-if="showEditDialog"
-                :title="textEditTitle"
-                :confirmText="textEditConfirm"
-                confirmClass="accept"
-                :closeText="textEditClose"
-                closeClass="cancel"
-                @close="closeEdit"
-                @confirm="storeContainer"
-                height="430"
-                width="680"
-            >
-                <template v-slot:dialogContent>
-                    <slot name="containerEditDialog"></slot>
-                </template>
-            </studip-dialog>
-
-            <studip-dialog
-                v-if="showChangeDialog"
-                :title="$gettext('Abschnitt verändern')"
-                :confirmText="$gettext('Speichern')"
-                confirmClass="accept"
-                :closeText="$gettext('Abbrechen')"
-                closeClass="cancel"
-                @close="closeChange"
-                @confirm="storeChange"
-                height="520"
-                width="480"
-            >
-                <template v-slot:dialogContent>
-                    <form class="default" @submit.prevent="">
-                        <div class="cw-radioset-wrapper" role="group" aria-labelledby="container-type">
-                            <p id="container-type">{{ $gettext('Typ') }}</p>
-                            <div class="cw-radioset">
-                                <div 
-                                    v-for="(container, index) in containerTypes"
-                                    :key="index"
-                                    class="cw-radioset-box" 
-                                    :class="[container.type === changeType ? 'selected' : '']"
-                                >
-                                    <input type="radio" :id="'type-' + container.type" :value="container.type" v-model="changeType" name="container-type"/>    
-                                    <label :for="'type-' + container.type" >
-                                        <div class="label-icon" :class="[container.type, container.type === changeType ? 'selected' : '']"></div>
-                                        <p>{{ container.title }}</p>
-                                    </label>
-                                    
-                                </div>
-                            </div>
-                        </div>
-                        <div class="cw-radioset-wrapper" role="group" aria-labelledby="container-style">
-                            <p id="container-style">{{ $gettext('Stil') }}</p>
-                            <div class="cw-radioset">
-                                <div
-                                v-for="(style, index) in containerStyles"
-                                :key="index"
-                                class="cw-radioset-box"
-                                :class="[style.colspan === changeStyle ? 'selected' : '']"
-                                >
-                                    <input type="radio" :id="'change-style-' + style.colspan" :value="style.colspan" v-model="changeStyle" name="change-container-style"/>
-                                    <label :for="'change-style-' + style.colspan">
-                                        <div class="label-icon" :class="[style.colspan, style.colspan === changeStyle ? 'selected' : '']"></div>
-                                        <p>{{ style.title }}</p>
-                                    </label>
-                                </div>
-                            </div>
-                        </div>
-                    </form>
-                </template>
-            </studip-dialog>
-
-
-            <studip-dialog
-                v-if="showDeleteDialog"
-                :title="textDeleteTitle"
-                :question="textDeleteAlert"
-                height="180"
-                width="380"
-                @confirm="executeDelete"
-                @close="closeDeleteDialog"
-            ></studip-dialog>
-
-            <studip-dialog
-                v-if="showRemoveLockDialog"
-                :title="textRemoveLockTitle"
-                :question="textRemoveLockAlert"
-                height="200"
-                width="450"
-                @confirm="executeRemoveLock"
-                @close="showRemoveLockDialog = false"
-            ></studip-dialog>
-
-        </div>
-    </div>
-</template>
-
-<script>
-import CoursewareContainerActions from './CoursewareContainerActions.vue';
-import StudipDialog from '../StudipDialog.vue';
-import { mapGetters, mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-default-container',
-    components: {
-        CoursewareContainerActions,
-        StudipDialog,
-    },
-    props: {
-        containerClass: String,
-        container: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            showDeleteDialog: false,
-            showEditDialog: false,
-            showChangeDialog: false,
-            showRemoveLockDialog: false,
-            textEditConfirm: this.$gettext('Speichern'),
-            textEditClose: this.$gettext('Schließen'),
-            textEditTitle: this.$gettext('Abschnitt bearbeiten'),
-            textDeleteTitle: this.$gettext('Abschnitt unwiderruflich löschen'),
-            textDeleteAlert: this.$gettext('Möchten Sie diesen Abschnitt wirklich löschen?'),
-            textRemoveLockTitle: this.$gettext('Sperre aufheben'),
-            textRemoveLockAlert: this.$gettext('Möchten Sie die Sperre dieses Abschnitts wirklich aufheben?'),
-            isOpen: true,
-
-            changeType: '',
-            changeStyle: '',
-        };
-    },
-    computed: {
-        ...mapGetters({
-            blockAdder: 'blockAdder',
-            userId: 'userId',
-            userById: 'users/byId',
-            viewMode: 'viewMode',
-            currentElementisLink: 'currentElementisLink',
-            containerTypes: 'containerTypes',
-        }),
-        showEditMode() {
-            return this.viewMode === 'edit' && !this.currentElementisLink;
-        },
-        colSpan() {
-            return this.container.attributes.payload.colspan ? this.container.attributes.payload.colspan : 'full';
-        },
-        blocked() {
-            return this.container?.relationships?.['edit-blocker']?.data !== null;
-        },
-        blockerId() {
-            return this.blocked ? this.container?.relationships?.['edit-blocker']?.data?.id : null;
-        },
-        blockedByThisUser() {
-            return this.blocked && this.userId === this.blockerId;
-        },
-        blockedByAnotherUser() {
-            return this.blocked && this.userId !== this.blockerId;
-        },
-        blockingUser() {
-            if (this.blockedByAnotherUser) {
-                return this.userById({id: this.blockerId});
-            }
-
-            return null;
-        },
-        blockingUserName() {
-            return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : '';
-        },
-        containerStyles() {
-            return [
-                { title: this.$gettext('Volle Breite'), colspan: 'full'},
-                { title: this.$gettext('Halbe Breite'), colspan: 'half' },
-                { title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' },
-            ];
-        },
-        type() {
-            return this.container.attributes['container-type'];
-        }
-    },
-    methods: {
-        ...mapActions({
-            companionInfo: 'companionInfo',
-            companionWarning: 'companionWarning',
-            companionSuccess: 'companionSuccess',
-            updateContainer: 'updateContainer',
-            loadContainer: 'courseware-containers/loadById',
-            deleteContainer: 'deleteContainer',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-            coursewareBlockAdder: 'coursewareBlockAdder',
-            createClipboard: 'courseware-clipboards/create'
-        }),
-        async displayEditDialog() {
-            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
-            if (this.blockedByAnotherUser) {
-                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
-
-                return false;
-            }
-
-            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-            this.showEditDialog = true;
-        },
-        async displayChangeDialog() {
-            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
-            if (this.blockedByAnotherUser) {
-                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
-
-                return false;
-            }
-
-            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-            this.changeType = this.type;
-            this.changeStyle = this.colSpan;
-            this.showChangeDialog = true;
-        },
-        async storeChange() {
-            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
-            this.closeChange();
-            if (this.blockedByAnotherUser) {
-                this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
-                    )
-                });
-                return;
-            }
-            if (this.blockerId === null) {
-                await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-            }
-
-            let container = this.container;
-            container.attributes['container-type'] = this.changeType;
-            container.attributes.payload.colspan = this.changeStyle;
-            await this.updateContainer({
-                container: container,
-                structuralElementId: this.container.relationships['structural-element'].data.id,
-            });
-            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
-            await this.loadContainer({id : this.container.id });
-        },
-        async closeChange() {
-            await this.loadContainer({ id: this.container.id });
-            this.showChangeDialog = false;
-            if (this.blockedByThisUser) {
-                await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
-            }
-            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
-        },
-        async closeEdit() {
-            await this.loadContainer({ id: this.container.id });
-            this.$emit('closeEdit');
-            this.showEditDialog = false;
-            if (this.blockedByThisUser) {
-                await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
-            }
-            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
-        },
-        async storeContainer() {
-            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
-            if (this.blockedByThisUser) {
-                this.$emit('storeContainer');
-            }
-            if (this.blockedByAnotherUser) {
-                this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
-                    )
-                });
-                this.$emit('closeEdit');
-            }
-            if (this.blockerId === null) {
-                await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-                this.$emit('storeContainer');
-            }
-            this.showEditDialog = false;
-        },
-        async displayDeleteDialog() {
-            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
-            if (!this.blocked) {
-                await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-                this.showDeleteDialog = true;
-            } else {
-                if (this.blockedByThisUser) {
-                    this.showDeleteDialog = true;
-                } else {
-                    this.companionInfo({
-                        info: this.$gettextInterpolate(
-                            this.$gettext('Löschen nicht möglich, da %{blockingUserName} den Abschnitt bearbeitet.'),
-                            {blockingUserName: this.blockingUserName}
-                        )
-                    });
-                }
-            }
-        },
-        async closeDeleteDialog() {
-            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
-            if (this.blockedByThisUser) {
-                await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
-            }
-            this.showDeleteDialog = false;
-        },
-        async executeDelete() {
-            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
-            if (this.blockedByAnotherUser) {
-                this.companionInfo({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
-                    )
-                });
-                return false;
-            }
-            await this.deleteContainer({
-                containerId: this.container.id,
-                structuralElementId: this.container.relationships['structural-element'].data.id,
-            });
-            if(Object.keys(this.blockAdder).length !== 0 && this.blockAdder.container.id === this.container.id) {
-                this.coursewareBlockAdder({});
-            }
-            this.showDeleteDialog = false;
-        },
-        displayRemoveLockDialog() {
-            this.showRemoveLockDialog = true;
-        },
-        async executeRemoveLock() {
-            await this.unlockObject({ id: this.container.id , type: 'courseware-containers' });
-            await this.loadContainer({ id: this.container.id });
-            this.showRemoveLockDialog = false;
-        },
-
-        async copyToClipboard() {
-            const clipboard = {
-                attributes: {
-                    name: this.container.attributes.title,
-                    'container-id': this.container.id,
-                    'object-type': this.container.type,
-                    'object-kind': this.container.attributes['container-type'],
-                    
-                }
-            };
-
-            await this.createClipboard(clipboard, { root: true });
-            this.companionSuccess({ info: this.$gettext('Abschnitt wurde in Merkliste abgelegt.') });
-        }
-
-    },
-
-    watch: {
-        showEditDialog(state) {
-            this.$emit('showEdit', state);
-        }
-    }
-
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareDialogCardsBlock.vue b/resources/vue/components/courseware/CoursewareDialogCardsBlock.vue
deleted file mode 100644
index 4df490b..0000000
--- a/resources/vue/components/courseware/CoursewareDialogCardsBlock.vue
+++ /dev/null
@@ -1,295 +0,0 @@
-<template>
-    <div class="cw-block cw-block-dialog-cards">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div class="cw-block-dialog-cards-content">
-                    <button
-                        class="cw-dialogcards-prev cw-dialogcards-navbutton"
-                        :class="{'cw-dialogcards-prev-disabled': hasNoPerv}"
-                        @click="prevCard"
-                        :title="hasNoPerv ? $gettext('keine vorherige Karte') : $gettext('zur vorherigen Karte')"
-                    >
-                    </button>
-                    <div class="cw-dialogcards">
-                        <div
-                            class="scene scene--card"
-                            :class="[card.active ? 'active' : '']"
-                            v-for="card in currentCards"
-                            :key="card.index"
-                        >
-                            <div
-                                class="card"
-                                tabindex="0"
-                                :title="$gettext('Karte umdrehen')"
-                                @click="flipCard"
-                                @keydown.enter="flipCard"
-                                @keydown.space="flipCard"
-                            >
-                                <div class="card__face card__face--front">
-                                    <img v-if="card.front_file.length !== 0" :src="card.front_file.download_url" />
-                                    <div v-else class="cw-dialogcards-front-no-image"></div>
-                                    <p>{{ card.front_text }}</p>
-                                </div>
-                                <div class="card__face card__face--back">
-                                    <img v-if="card.back_file.length !== 0" :src="card.back_file.download_url" />
-                                    <div v-else class="cw-dialogcards-back-no-image"></div>
-                                    <p>{{ card.back_text }}</p>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                    <button
-                        class="cw-dialogcards-next cw-dialogcards-navbutton"
-                        :class="{'cw-dialogcards-next-disabled': hasNoNext}"
-                        @click="nextCard"
-                        :title="hasNoNext ? $gettext('keine nächste Karte') : $gettext('zur nächsten Karte')"
-                    >
-                    </button>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <button class="button add" @click="addCard">{{ $gettext('Karte hinzufügen') }}</button>
-                <courseware-tabs
-                    v-if="currentCards.length > 0"
-                    :setSelected="setCardTab"
-                    @selectTab="activateCard(parseInt($event.name.replace($gettext('Karte') +  ' ', '')) - 1)"
-                >
-                    <courseware-tab
-                        v-for="(card, index) in currentCards"
-                        :key="index"
-                        :index="index"
-                        :name="$gettext('Karte') +  ' ' + (index + 1).toString()"
-                        :selected="index === 0"
-                        canBeEmpty
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Bild Vorderseite') }}:
-                                <courseware-file-chooser
-                                    v-model="card.front_file_id"
-                                    :isImage="true"
-                                    :canBeEmpty="true"
-                                    @selectFile="updateFile(index, 'front', $event)"
-                                />
-                            </label>
-                            <label>
-                                {{ $gettext('Text Vorderseite') }}:
-                                <input type="text" v-model="card.front_text" />
-                            </label>
-                            <label>
-                                {{ $gettext('Bild Rückseite') }}:
-                                <courseware-file-chooser
-                                    v-model="card.back_file_id"
-                                    :isImage="true"
-                                    :canBeEmpty="true"
-                                    @selectFile="updateFile(index, 'back', $event)"
-                                />
-                            </label>
-                            <label>
-                                {{ $gettext('Text Rückseite') }}:
-                                <input type="text" v-model="card.back_text" />
-                            </label>
-                            <label v-if="!onlyCard">
-                                <button class="button trash" @click="removeCard(index)">
-                                    {{ $gettext('Karte entfernen') }}
-                                </button>
-                            </label>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Lernkarten-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFileChooser from './CoursewareFileChooser.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-dialog-cards-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareFileChooser,
-        CoursewareTabs,
-        CoursewareTab,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentCards: [],
-            setCardTab: 0
-        };
-    },
-    computed: {
-        cards() {
-            return this.block?.attributes?.payload?.cards;
-        },
-        onlyCard() {
-            return this.currentCards.length === 1;
-        },
-        hasNoPerv() {
-            if(this.currentCards[0] !== undefined) {
-                return this.currentCards[0].active;
-            } else {
-                return true;
-            }
-        },
-        hasNoNext() {
-            if (this.currentCards[this.currentCards.length - 1] !== undefined) {
-                return this.currentCards[this.currentCards.length - 1].active;
-            } else {
-                return true;
-            }
-        }
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            if (this.cards !== '') {
-                let cards = JSON.parse(JSON.stringify(this.cards));
-                cards.forEach((card, index) => {
-                    card.active = false;
-                    if (index === 0) {
-                        card.active = true;
-                    }
-                });
-                this.currentCards = cards;
-            }
-            this.setCardTab = 0;
-        },
-        storeBlock() {
-            let cards = JSON.parse(JSON.stringify(this.currentCards));
-            // don't store the file object
-            cards.forEach((card) => {
-                delete card.front_file;
-                delete card.back_file;
-                delete card.active;
-            });
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.cards = cards;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-        updateFile(cardIndex, side, file) {
-            if (side === 'front') {
-                if (file) {
-                    this.currentCards[cardIndex].front_file_id = file.id;
-                    this.currentCards[cardIndex].front_file = file;
-                }
-                else {
-                    this.currentCards[cardIndex].front_file_id = '';
-                    this.currentCards[cardIndex].front_file = [];
-                }
-            }
-            if (side === 'back') {
-                if (file) {
-                    this.currentCards[cardIndex].back_file_id = file.id;
-                    this.currentCards[cardIndex].back_file = file;
-                } else {
-                    this.currentCards[cardIndex].back_file_id = '';
-                    this.currentCards[cardIndex].back_file = [];
-                }
-            }
-        },
-        addCard() {
-            this.currentCards.push({
-                index: this.currentCards.length,
-                front_file_id: '',
-                front_file: [],
-                front_text: '',
-                back_file_id: '',
-                back_text: '',
-                back_file: [],
-                active: false
-            });
-            const index = this.currentCards.length - 1;
-            this.activateCard(index);
-            this.$nextTick(() => { this.setCardTab = index; });
-        },
-        removeCard(cardIndex){
-            this.currentCards = this.currentCards.filter((val, index) => {
-                return !(index === cardIndex);
-            });
-            this.$nextTick(() => { this.setCardTab = 0; });
-        },
-        flipCard(event) {
-            event.currentTarget.classList.toggle('is-flipped');
-        },
-        nextCard() {
-            let view = this;
-            this.currentCards.every((card, index) => {
-                if (card.active) {
-                    if (view.currentCards.length > index + 1) {
-                        card.active = false;
-                        view.currentCards[index + 1].active = true;
-                        view.setCardTab = index + 1;
-                    }
-                    return false; // end every
-                } else {
-                    return true; // continue every
-                }
-            });
-        },
-        prevCard() {
-            let view = this;
-            this.currentCards.every((card, index) => {
-                if (card.active) {
-                    if (index > 0) {
-                        card.active = false;
-                        view.currentCards[index - 1].active = true;
-                        view.setCardTab = index - 1;
-                    }
-                    return false; // end every
-                } else {
-                    return true; // continue every
-                }
-            });
-        },
-        activateCard(selectedIndex) {
-            selectedIndex = parseInt(selectedIndex);
-            if (selectedIndex > this.currentCards.length - 1) {
-                console.log('can not select this card');
-                return false;
-            }
-            this.currentCards.forEach((card, index) => {
-                if (index === selectedIndex) {
-                    card.active = true;
-                } else {
-                    card.active = false;
-                }
-            });
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/CoursewareDocumentBlock.vue
deleted file mode 100644
index 58b0af7..0000000
--- a/resources/vue/components/courseware/CoursewareDocumentBlock.vue
+++ /dev/null
@@ -1,771 +0,0 @@
-<template>
-    <div class="cw-block cw-block-document">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="false"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div class="cw-pdf-main-container">
-                    <template v-if="hasFile">
-                        <div v-if="currentTitle !== ''" class="cw-block-title">
-                            {{ currentTitle }}
-                        </div>
-                        <div class="cw-pdf-toolbar">
-                            <div class="cw-pdf-toolbar-left">
-                                <div class="cw-pdf-toc">
-                                    <button
-                                        class="undecorated"
-                                        :class="{active: pdfTOCDisplay}"
-                                        :title="$gettext('Inhaltsverzeichnis')"
-                                        :aria-pressed="pdfTOCDisplay ? 'true' : 'false'"
-                                        @click="toggleTOCViewer"
-                                    >
-                                        <studip-icon
-                                            shape="table-of-contents"
-                                            :role="pdfTOC.length === 0 ? 'inactive' : pdfTOCDisplay ? 'info_alt' :'clickable'"
-                                            :size="18"
-                                            class="text-bottom"
-                                        />
-                                    </button>
-                                </div>
-                                <div class="cw-pdf-search-toggle-btn">
-                                    <button
-                                        class="undecorated"
-                                        :class="{active: showPdfSearchBox}"
-                                        :title="$gettext('Suche')"
-                                        :aria-pressed="showPdfSearchBox ? 'true' : 'false'"
-                                        @click="togglePdfSearchBox"
-                                    >
-                                        <studip-icon
-                                            shape="search"
-                                            :role="showPdfSearchBox ? 'info_alt' : 'clickable'"
-                                            :size="18"
-                                            class="text-bottom"
-                                        />
-                                    </button>
-                                </div>
-                                <div class="cw-pdf-search-box" v-show="showPdfSearchBox">
-                                    <input ref="pdfSearchInput" type="text" v-model="pdfSearch" @change="doSearchInPdf">
-                                    <div class="cw-pdf-search-navs" v-if="pdfSearchFoundNums > 1">
-                                        <button class="undecorated" @click="prevPdfSearch" :title="$gettext('Letzte')">
-                                            <studip-icon
-                                                shape="arr_1left"
-                                                :role="pdfSearchFoundSelectedIndex === 0 ? 'inactive' : 'clickable'"
-                                                :size="18"
-                                                class="text-bottom"
-                                            />
-                                        </button>
-                                        <button class="undecorated" @click="nextPdfSearch" :title="$gettext('Nächste')">
-                                            <studip-icon
-                                                shape="arr_1right"
-                                                :role="pdfSearchFoundSelectedIndex === pdfSearchFoundNums - 1 ? 'inactive' : 'clickable'"
-                                                :size="18"
-                                                class="text-bottom"
-                                            />
-                                        </button>
-                                    </div>
-                                    <span class="cw-pdf-search-num" v-if="pdfSearchFoundNums > 0">
-                                        {{ (pdfSearchFoundSelectedIndex + 1) }} / {{ pdfSearchFoundNums }} {{ $gettext('Treffer') }}
-                                    </span>
-                                </div>
-                                <div class="cw-pdf-page-nav">
-                                    <button class="undecorated" @click="prevPage" :title="$gettext('Eine Seite zurück')">
-                                        <studip-icon
-                                            shape="arr_1up"
-                                            :role="pageNum - 1 === 0 ? 'inactive' : 'clickable'"
-                                            :size="18"
-                                            class="text-bottom"
-                                        />
-                                    </button>
-                                    <button class="undecorated" @click="nextPage" :title="$gettext('Eine Seite vor')">
-                                        <studip-icon
-                                            shape="arr_1down"
-                                            :role="pageNum === pageCount ? 'inactive' : 'clickable'"
-                                            :size="18"
-                                            class="text-bottom"
-                                        />
-                                    </button>
-                                    <input
-                                        type="text"
-                                        ref="pageNumInput"
-                                        class="cw-pdf-page-num"
-                                        :aria-label="$gettext('Seite')"
-                                        :value="pageNum"
-                                        @change="updatePageNum"
-                                    >
-                                    <span>
-                                        {{ $gettext('von') }} {{ pageCount }}
-                                    </span>
-                                </div>
-                            </div>
-                            <div class="cw-pdf-toolbar-middle">
-                                <div class="cw-pdf-zoom-buttons">
-                                    <button class="undecorated" @click="zoomIn" :title="$gettext('Vergrößern')">
-                                        <studip-icon shape="add" :size="18" class="text-bottom" />
-                                    </button>
-                                    <button class="undecorated" @click="zoomOut" :title="$gettext('Verkleinern')">
-                                        <studip-icon shape="remove" :size="18" class="text-bottom" />
-                                    </button>
-                                    <select v-model="currentScale" :aria-label="$gettext('Zoom')" @change="updateZoom">
-                                        <option v-show="false" :value="currentScale">{{ formattedZoom }}%</option>
-                                        <option v-for="(value, index) in scaleValues" :key="index" :value="value">{{ value * 100 }}%</option>
-                                    </select>
-                                </div>
-                                <div class="cw-pdf-rotate">
-                                    <button class="undecorated" @click="doRotatePdf" :title="$gettext('Drehen')">
-                                        <studip-icon shape="rotate-right" :size="18" class="text-bottom" />
-                                    </button>
-                                </div>
-                            </div>
-                            <div class="cw-pdf-toolbar-right">
-                                <div class="cw-pdf-handtool">
-                                    <button
-                                        class="undecorated"
-                                        :class="{active: pdfHandTool}"
-                                        :title="$gettext('Hand-Werkzeug')"
-                                        :aria-pressed="pdfHandTool ? 'true' : 'false'"
-                                        @click="toggleHandTool"
-                                    >
-                                        <studip-icon
-                                            shape="hand"
-                                            :role="pdfHandTool ? 'info_alt' : 'clickable'"
-                                            :size="18"
-                                            class="text-bottom"
-                                        />
-                                    </button>
-                                </div>
-                                <div class="cw-pdf-download">
-                                    <a v-if="downloadable === 'true'" :href="currentUrl" download :title="$gettext('Speichern')">
-                                        <studip-icon shape="download" :size="18" class="text-bottom"/>
-                                    </a>
-                                </div>
-                            </div>
-                        </div>
-                        <div class="cw-pdf-outer-container" ref="outerContainer">
-                            <div class="cw-pdf-content">
-                                <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
-                                <div class="cw-pdf-sidebar" v-show="pdfTOCDisplay">
-                                    <ul class="cw-pdf-toc-list">
-                                        <CoursewarePDFTableOfContent v-for="(item, index) in pdfTOC" :item="item" :key="index" @tocPageNav="tocPageNav" />
-                                    </ul>
-                                </div>
-                                <div
-                                    ref="container"
-                                    class="cw-pdf-viewer-container"
-                                    :class="{'hand-cursor-grab': pdfHandTool, 'grabbing':  pdfGrabbing, 'has-error': pdfError}"
-                                    v-dragscroll="pdfHandTool"
-                                    @mousedown="handleMouseDown"
-                                    @mouseup="handleMouseUp"
-                                >
-                                    <div class="pdfViewer"/>
-                                </div>
-                            </div>
-                            <div v-show="pdfError" class="cw-pdf-error-page">
-                                <courseware-companion-box 
-                                    mood="sad"
-                                    :msgCompanion="$gettext('Es gab einen Fehler. Bitte versuchen Sie es erneut!')"
-                                >
-                                </courseware-companion-box>
-                            </div>
-                            <div ref="fakeContainer" class="cw-pdf-viewer-fake-container">
-                                <div class="pdfViewer"/>
-                            </div>
-                        </div>
-                    </template>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Überschrift') }}
-                        <input type="text" v-model="currentTitle" />
-                    </label>
-                    <label>
-                        {{ $gettext('Datei') }}
-                        <courseware-file-chooser
-                            v-model="currentFileId"
-                            :isDocument="true"
-                            @selectFile="updateCurrentFile"
-                        />
-                    </label>
-                    <label>
-                        {{ $gettext('Download-Icon anzeigen') }}
-                        <select v-model="currentDownloadable">
-                            <option value="true">{{ $gettext('Ja') }}</option>
-                            <option value="false">{{ $gettext('Nein') }}</option>
-                        </select>
-                    </label>
-                    <label>
-                        {{ $gettext('Dateityp') }}
-                        <select v-model="currentDocType">
-                            <option value="pdf">PDF</option>
-                        </select>
-                    </label>
-                </form>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Dokument-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFileChooser from './CoursewareFileChooser.vue';
-import CoursewarePDFTableOfContent from './CoursewarePDFTableOfContent.vue';
-import { blockMixin } from './block-mixin.js';
-import { getDocument } from 'pdfjs-dist';
-import {
-    DefaultAnnotationLayerFactory,
-    DefaultTextLayerFactory,
-    DefaultXfaLayerFactory,
-    DefaultStructTreeLayerFactory,
-    PDFFindController,
-    PDFLinkService,
-    PDFPageView,
-    PDFViewer,
-    EventBus
-} from 'pdfjs-dist/web/pdf_viewer.js';
-// pdfjsWorker must be imported!
-import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
-import { dragscroll } from 'vue-dragscroll'
-
-import { mapActions } from 'vuex';
-import 'pdfjs-dist/web/pdf_viewer.css';
-
-export default {
-    name: 'courseware-document-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareCompanionBox,
-        CoursewareDefaultBlock,
-        CoursewareFileChooser,
-        CoursewarePDFTableOfContent
-    },
-    directives: {
-        dragscroll
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentFileId: '',
-            currentFile: {},
-            currentDownloadable: '',
-            currentDocType: '',
-
-            pdfError: false,
-            pdfBasePage: null,
-            pdfPage: null,
-            pdfTextContent: null,
-            pdfHandTool: false,
-            pdfGrabbing: false,
-            pdfTextLayer: null,
-            pdfAnnotationLayer: null,
-            pdfAnnotation: false,
-            pdfRotate: 0,
-            PdfViewer: null,
-            pdfEventBus: null,
-            pdfLinkService: null,
-            pdfFindController: null,
-            pdfDoc: null,
-            pdfLoadingTask: null,
-            pdfSearch: '',
-            pdfSearchMatchesMapping: [],
-            pdfSearchFoundNums: 0,
-            pdfSearchFoundSelectedIndex: 0,
-            pdfSearchHighlightedList: [],
-            showPdfSearchBox: false,
-            pdfTOC: [],
-            pdfTOCDisplay: false,
-            pageNum: 1,
-            pageCount: 0,
-            scale: 1,
-            currentScale: 1,
-            scaleValues: [0.5, 1, 1.5, 2, 3, 4],
-            file: null,
-
-            srMessage: ''
-        };
-    },
-    computed: {
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        fileDownloadable() {
-            return this.currentDownloadable === 'true';
-        },
-        downloadable() {
-            return this.block?.attributes?.payload?.downloadable ?? 'true';
-        },
-        fileId() {
-            return this.block?.attributes?.payload?.file_id;
-        },
-        docType() {
-            return this.block?.attributes?.payload?.doc_type;
-        },
-        currentUrl() {
-            if (this.currentFile?.meta) {
-                return this.currentFile.meta['download-url'];
-            } else {
-                return '';
-            }
-        },
-        hasFile() {
-            return this.currentFileId !== '';
-        },
-        formattedZoom () {
-            return Number.parseInt(this.scale * 100, 10);
-        },
-    },
-    watch: {
-        scale(newValue) {
-            let overflow = newValue > 1 ? 'auto' : 'hidden';
-            let container = this.$refs.container;
-            container.style.overflow = overflow;
-            this.currentScale = newValue;
-        },
-        pageNum(newValue) {
-            this.resetPdfViewer();
-        },
-        showPdfSearchBox() {
-            this.resetPdfSearch();
-        }
-    },
-    mounted() {
-        this.loadFileRefs(this.block.id).then((response) => {
-            this.file = response[0];
-            this.currentFile = this.file;
-            this.initPdfTask();
-        });
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            loadFileRefs: 'loadFileRefs',
-            companionWarning: 'companionWarning',
-        }),
-        initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentDownloadable = this.downloadable;
-            this.currentFileId = this.fileId;
-            this.currentDocType = this.docType;
-        },
-        updateCurrentFile(file) {
-            this.currentFile = file;
-            this.currentFileId = file.id;
-        },
-        initPdfTask() {
-            if (this.currentUrl) {
-                let view = this;
-                view.pdfEventBus = new EventBus();
-                view.pdfLoadingTask = getDocument(this.currentUrl).promise;
-                view.pdfLoadingTask.__PDFDocumentLoadingTask = true;
-                // Link Service
-                view.pdfLinkService = new PDFLinkService({
-                    eventBus: view.pdfEventBus,
-                });
-                // Find Controller
-                view.pdfFindController = new PDFFindController({
-                    eventBus: view.pdfEventBus,
-                    linkService: view.pdfLinkService
-                });
-                // Annotation Layer
-                view.pdfAnnotationLayer = new DefaultAnnotationLayerFactory();
-                // Text Layer
-                view.pdfTextLayer = new DefaultTextLayerFactory();
-                // Load Pdf Document
-                view.loadPdfDocument();
-
-                // Handle search results.
-                view.pdfEventBus.on('updatetextlayermatches', ({source, pageIndex}) => {
-                    if (view.pdfViewer.pdfPage._pageIndex == pageIndex) {
-                        setTimeout(() => {
-                            view.handleSearchMatches();
-                        }, 260);
-                    }
-                });
-            }
-        },
-        loadPdfDocument() {
-            if (this.pdfLoadingTask) {
-                let view = this;
-                view.pdfLoadingTask.then((pdfDocument) => {
-                    view.pdfDoc = pdfDocument;
-                    view.pageCount = pdfDocument.numPages;
-                    // get table of contents if any.
-                    view.loadPdfTOC();
-                    // Rendering PDF viewer
-                    view.loadPdfViewer();
-                    view.pdfLinkService.setDocument(view.pdfDoc, null);
-                    view.pdfFindController.setDocument(view.pdfDoc);
-                });
-            }
-        },
-        loadPdfTOC() {
-            if (this.pdfDoc) {
-                let view = this;
-                view.pdfTOC = [];
-                // Get the tree outline
-                view.pdfDoc.getOutline().then((outline) => {
-                    if (outline) {
-                        view.pdfTOC = outline;
-                    }
-                });
-            }
-        },
-        loadPdfViewer() {
-            if (this.pdfDoc) {
-                let view = this;
-                this.pdfError = false;
-                let container = this.$refs.container;
-                let outerContainer = this.$refs.outerContainer;
-                let fakeContainer = this.$refs.fakeContainer;
-                this.pdfDoc.getPage(parseInt(view.pageNum)).then((pdfPage) => {
-                    view.pdfPage = pdfPage;
-                    // Creating the page view with default parameters.
-                    let defaultViewport = pdfPage.getViewport({
-                        scale: 1.35,
-                    });
-
-                    view.pdfBasePage = new PDFViewer({
-                        container: fakeContainer,
-                        eventBus: view.pdfEventBus,
-                        findController: view.pdfFindController
-                    });
-
-                    let pdfPageViewOptions = {
-                        container: container,
-                        id: view.pageNum,
-                        scale: view.scale,
-                        defaultViewport: defaultViewport,
-                        eventBus: view.pdfEventBus,
-                        findController: view.pdfFindController,
-                        textHighlighterFactory: view.pdfBasePage,
-                        xfaLayerFactory: view.pdfDoc.isPureXfa
-                            ? new DefaultXfaLayerFactory()
-                            : null,
-                        structTreeLayerFactory: new DefaultStructTreeLayerFactory()
-                    };
-                    if (view.pdfHandTool === false) {
-                        pdfPageViewOptions.textLayerFactory = view.pdfTextLayer;
-                        pdfPageViewOptions.annotationLayerFactory = view.pdfAnnotationLayer;
-                    } else {
-                        pdfPageViewOptions.textLayerMode = 0;
-                        pdfPageViewOptions.annotationMode = 0;
-                    }
-                    // Force annotation to be disabled.
-                    if (!this.pdfAnnotation && pdfPageViewOptions?.annotationLayerFactory) {
-                        pdfPageViewOptions.annotationLayerFactory = null;
-                        pdfPageViewOptions.annotationMode = 0;
-                    }
-                    view.pdfViewer = new PDFPageView(pdfPageViewOptions);
-                    // Associates the actual page with the view, and drawing it
-                    view.pdfViewer.setPdfPage(view.pdfPage);
-                    // Set LinkService viewer
-                    view.pdfLinkService.setViewer(view.pdfViewer);
-                    // Set outer container height
-                    outerContainer.style.height = container.offsetHeight + 'px';
-                    view.renderPage();
-                }).catch(err => {
-                    console.log(err);
-                    outerContainer.style.minHeight = '350px';
-                    view.pdfError = true;
-                });
-            }
-        },
-        renderPage() {
-            if (this.pdfViewer) {
-                this.updatePdfViewer();
-                this.pdfViewer.draw();
-                if (!this.pdfHandTool) {
-                    this.pdfViewer.textLayer.findController = this.pdfFindController;
-                }
-                if (this.pdfPage) {
-                    this.pdfPage.getTextContent().then((textContent) => {
-                        this.pdfTextContent = textContent;
-                    });
-                }
-                if (this.pdfSearchMatchesMapping.length) {
-                    this.pdfSearchDisplayHandler();
-                }
-            }
-        },
-        resetPdfViewer() {
-            this.pdfViewer.destroy();
-            let container = this.$refs.container;
-            while (!container.lastChild.classList.contains('pdfViewer')) {
-                container.removeChild(container.lastChild);
-            }
-            this.loadPdfViewer();
-        },
-        updatePdfViewer(resetScale = false) {
-            let updateArgs = {
-                scale: resetScale ? 1 : this.scale,
-                rotation: this.pdfRotate,
-            };
-            this.pdfViewer.update(updateArgs);
-        },
-        prevPage() {
-            if (this.pageNum <= 1) {
-                return;
-            }
-            this.pageNum--;
-        },
-        nextPage() {
-            if (this.pageNum >= this.pdfDoc.numPages) {
-                return;
-            }
-            this.pageNum++;
-        },
-        goToPage(page) {
-            const pageNum = Number.parseInt(page, 10);
-            if (pageNum < 1 || pageNum > this.pdfDoc.numPages) {
-                return;
-            }
-            this.pageNum = pageNum;
-        },
-        tocPageNav(dest) {
-            let view = this;
-            let destObj = dest.find((ref) =>
-                typeof ref === "object" && ref !== null &&
-                Number.isInteger(ref.num) && ref.num >= 0 &&
-                Number.isInteger(ref.gen) && ref.gen >= 0);
-            if (destObj) {
-                view.pdfDoc.getPageIndex(destObj).then((pageIndex) => {
-                    view.goToPage(pageIndex + 1);
-                });
-            }
-        },
-        updatePageNum() {
-            let pageNumInput = this.$refs.pageNumInput;
-            let value = Number.parseInt(pageNumInput.value, 10);
-            if (Number.isInteger(value) && value > 0 && value <= Number.parseInt(this.pageCount, 10)) {
-                this.pageNum = value;
-            } else {
-                pageNumInput.value = this.pageNum;
-            }
-        },
-        doRotatePdf() {
-            let rotationDegs = [0, 90, 180, 270, 360];
-            let index = rotationDegs.indexOf(this.pdfRotate);
-            let nextIndex = index + 1 >= rotationDegs.length ? 0 : index + 1;
-            let nextDeg = rotationDegs[nextIndex];
-            this.pdfRotate = nextDeg;
-            this.renderPage();
-            this.updateSrMessage(this.$gettext('gedreht'));
-        },
-        zoomIn() {
-            this.scale = this.scale < 4 ? (this.scale * 10 + 1) / 10 : this.scale;
-            this.renderPage();
-            this.updateSrMessage(this.$gettext('vergrößert'));
-        },
-        zoomOut() {
-            this.scale = this.scale > 0.1 ? (this.scale * 10 - 1) / 10 : this.scale;
-            this.renderPage();
-            this.updateSrMessage(this.$gettext('verkleinert'));
-        },
-        updateZoom(e) {
-            const value = e.target.value;
-            if (this.scale === value) {
-                return;
-            }
-            this.scale = value;
-            this.renderPage();
-            this.updateSrMessage(this.$gettext('Zoom Stufe ausgweählt'));
-        },
-        toggleHandTool() {
-            this.pdfHandTool = !this.pdfHandTool;
-            this.resetPdfViewer();
-            this.showPdfSearchBox = false;
-        },
-        handleHandToolDisplay(event) {
-            this.pdfGrabbing = event.type === 'mousedown';
-        },
-        handleMouseDown(e) {
-            this.handleHandToolDisplay(e);
-        },
-        handleMouseUp(e) {
-            this.handleHandToolDisplay(e);
-        },
-        togglePdfSearchBox() {
-            this.showPdfSearchBox = this.pdfHandTool ? false : !this.showPdfSearchBox;
-            if (this.showPdfSearchBox) {
-                this.$nextTick(() => {
-                    this.$refs.pdfSearchInput.focus();
-                });
-            }
-        },
-        handleSearchMatches() {
-            let view = this;
-            let allMatches = view.pdfFindController.pageMatches;
-            let totalMatches = 0;
-            let searchSelectIndex = 0;
-            let matchesPageCount = 0;
-            view.pdfSearchMatchesMapping = [];
-            for (let pageIndex = 0; pageIndex < view.pageCount; pageIndex++) {
-                let pageNum = pageIndex + 1;
-                let pageMatches = allMatches[pageIndex];
-                totalMatches += pageMatches.length;
-                if (pageMatches.length) {
-                    matchesPageCount++;
-                }
-                for (let i in pageMatches) {
-                    let matchIndex = parseInt(i, 10);
-                    let mappingObj = {
-                        selectIndex: searchSelectIndex,
-                        matchIndex: matchIndex,
-                        pageNum: pageNum,
-                    }
-                    view.pdfSearchMatchesMapping.push(mappingObj);
-                    searchSelectIndex++;
-                }
-            }
-            // Find next match if there the current page has nothing.
-            if (
-                view.pdfSearchFoundSelectedIndex === 0
-                && view.pdfViewer.pdfPage._pageIndex > 0
-                && matchesPageCount > 0
-            ) {
-                let nextMapped = view.pdfSearchMatchesMapping.filter(map => map.pageNum >= view.pdfViewer.pdfPage._pageIndex + 1);
-                if (nextMapped.length) {
-                    view.pdfSearchFoundSelectedIndex = nextMapped[0].selectIndex;
-                }
-            }
-            view.pdfSearchFoundNums = totalMatches;
-            view.pdfSearchDisplayHandler();
-        },
-        doSearchInPdf() {
-            let findObj = {
-                type: '',
-                query: this.pdfSearch,
-                phraseSearch: true,
-                caseSensitive: false,
-                entireWord: true,
-                highlightAll: true,
-                findPrevious: false,
-                matchDiacritics: false
-            };
-            this.pdfEventBus.dispatch('find', findObj);
-        },
-        prevPdfSearch() {
-            if (this.pdfSearchFoundSelectedIndex === 0) {
-                return;
-            }
-            this.pdfSearchFoundSelectedIndex--;
-            this.pdfSearchDisplayHandler();
-        },
-        nextPdfSearch() {
-            if (this.pdfSearchFoundSelectedIndex === this.pdfSearchFoundNums - 1) {
-                return;
-            }
-            this.pdfSearchFoundSelectedIndex++;
-            this.pdfSearchDisplayHandler();
-        },
-        pdfSearchDisplayHandler() {
-            // Go to page based on selected index.
-            let pageMatches = this.pdfSearchMatchesMapping.filter(map => 
-                map.selectIndex === this.pdfSearchFoundSelectedIndex
-            );
-            if (pageMatches.length) {
-                let matchObj = pageMatches[0];
-                // A timeout of > 250ms is needed when page is changed!
-                let highlightRenderTimeout = 0;
-                if (matchObj.pageNum !== this.pageNum) {
-                    this.goToPage(matchObj.pageNum);
-                    highlightRenderTimeout = 260;
-                }
-                setTimeout(() => {
-                    this.setPdfSearchHighlighted();
-                    this.scrollToSearchFounds(matchObj.matchIndex);
-                }, highlightRenderTimeout);
-            }
-        },
-        scrollToSearchFounds(matchIndex) {
-            if (this.pdfSearchHighlightedList?.length) {
-                let selectedSpan = this.pdfSearchHighlightedList[matchIndex];
-                if (selectedSpan) {
-                    selectedSpan.classList.add('selected');
-                    selectedSpan.scrollIntoView({ behavior: 'smooth', block: "center" });
-                }
-            }
-        },
-        setPdfSearchHighlighted() {
-            if (this.pdfViewer?.textLayer?.textDivs) {
-                let textDivs = this.pdfViewer.textLayer.textDivs;
-                let highlightedSpans = [];
-                for (let textSpan of textDivs) {
-                    if (textSpan?.children) {
-                        let children = [...textSpan.children];
-                        for (let child of children) {
-                            if (child.nodeName == 'SPAN' && child.classList.contains('highlight')) {
-                                child.classList.remove('selected');
-                                highlightedSpans.push(child);
-                            }
-                        }
-                    }
-                }
-                // Sort the array based on the top of the span.
-                highlightedSpans.sort((current, next) => {
-                    let currentTop = parseInt(current.parentNode.style.top, 10);
-                    let nextTop = parseInt(next.parentNode.style.top, 10);
-                    return currentTop > nextTop;
-                });
-                this.pdfSearchHighlightedList = highlightedSpans;
-            }
-        },
-        resetPdfSearch() {
-            this.pdfSearch = '';
-            this.pdfSearchFoundNums = 0;
-            this.pdfSearchFoundSelectedIndex = 0;
-            this.pdfSearchHighlightedList = [];
-            this.pdfSearchMatchesMapping = [];
-            this.doSearchInPdf();
-        },
-        toggleTOCViewer() {
-            if (this.pdfTOC.length) {
-                this.pdfTOCDisplay = !this.pdfTOCDisplay;
-            } else {
-                this.pdfTOCDisplay = false;
-            }
-        },
-        storeBlock() {
-            if (this.currentFile === undefined) {
-                this.companionWarning({
-                    info: this.$gettext('Bitte wählen Sie eine Datei aus.')
-                });
-                return false;
-            } else {
-                let attributes = {};
-                attributes.payload = {};
-                attributes.payload.title = this.currentTitle;
-                attributes.payload.file_id = this.currentFile.id;
-                attributes.payload.downloadable = this.currentDownloadable.toString();
-                attributes.payload.doc_type = this.currentDocType;
-
-                this.updateBlock({
-                    attributes: attributes,
-                    blockId: this.block.id,
-                    containerId: this.block.relationships.container.data.id,
-                });
-            }
-        },
-        updateSrMessage(message) {
-            this.srMessage = '';
-            this.srMessage = message;
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareDownloadBlock.vue b/resources/vue/components/courseware/CoursewareDownloadBlock.vue
deleted file mode 100644
index e642e79..0000000
--- a/resources/vue/components/courseware/CoursewareDownloadBlock.vue
+++ /dev/null
@@ -1,296 +0,0 @@
-<template>
-    <div class="cw-block cw-block-download">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            :defaultGrade="false"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
-                <div v-if="currentFile !== null" class="cw-block-download-content">
-                    <div v-if="currentInfo !== '' && !userHasDownloaded" class="messagebox messagebox_info">
-                        {{ currentInfo }}
-                    </div>
-                    <div v-if="currentSuccess !== '' && userHasDownloaded" class="messagebox messagebox_info">
-                        {{ currentSuccess }}
-                    </div>
-                    <div class="cw-block-download-file-item">
-                        <a
-                            target="_blank"
-                            :download="currentFile.name"
-                            :title="$gettext('Datei herunterladen')"
-                            :href="currentFile.download_url"
-                            @click="handleDownload"
-                        >
-                            <span class="cw-block-file-info" :class="['cw-block-file-icon-' + currentFile.icon]">
-                                {{ currentFile.name }}
-                            </span>
-                        </a>
-                    </div>
-                </div>
-                <div v-else class="cw-block-download-content">
-                    <div class="cw-block-download-file-item-not-available">
-                        <span class="cw-block-file-info cw-block-file-icon-none">
-                            {{ $gettext('Datei ist nicht verfügbar') }}
-                        </span>
-                    </div>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <courseware-tabs>
-                    <courseware-tab
-                        :index="0"
-                        :name="$gettext('Datei')"
-                        :selected="true"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Überschrift') }}
-                                <input type="text" v-model="currentTitle" />
-                            </label>
-                            <label>
-                                {{ $gettext('Datei') }}
-                                <courseware-file-chooser v-model="currentFileId" @selectFile="updateCurrentFile" />
-                            </label>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="1"
-                        :name="$gettext('Infobox')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Infobox vor Download') }}
-                                <input type="text" v-model="currentInfo" />
-                            </label>
-                            <label>
-                                {{ $gettext('Infobox nach Download') }}
-                                <input type="text" v-model="currentSuccess" />
-                            </label>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="2"
-                        :name="$gettext('Fortschritt')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Fortschritt erst beim Herunterladen') }}
-                                <select v-model="currentGrade">
-                                    <option value="false">{{ $gettext('Nein') }}</option>
-                                    <option value="true">{{ $gettext('Ja') }}</option>
-                                </select>
-                            </label>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Download-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFileChooser from './CoursewareFileChooser.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-download-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareFileChooser,
-        CoursewareTabs,
-        CoursewareTab,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentInfo: '',
-            currentSuccess: '',
-            currentGrade: '',
-            currentFileId: '',
-            currentFile: null,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            fileRefById: 'file-refs/byId',
-            urlHelper: 'urlHelper',
-            relatedTermOfUse: 'terms-of-use/related',
-            getUserDataById: 'courseware-user-data-fields/byId',
-        }),
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        info() {
-            return this.block?.attributes?.payload?.info;
-        },
-        success() {
-            return this.block?.attributes?.payload?.success;
-        },
-        grade() {
-            return this.block?.attributes?.payload?.grade;
-        },
-        fileId() {
-            return this.block?.attributes?.payload?.file_id;
-        },
-        userData() {
-            return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id });
-        },
-        userHasDownloaded() {
-            let downloaded = this.userData?.attributes?.payload?.downloaded;
-
-            if (downloaded === undefined) {
-                return false;
-            }
-
-            return downloaded;
-        }
-    },
-    mounted() {
-        this.initCurrentData();
-        if (this.userProgress && this.userProgress.attributes.grade === 0 && !this.grade) {
-            this.userProgress = 1;
-        }
-    },
-    methods: {
-        ...mapActions({
-            loadFileRef: 'file-refs/loadById',
-            updateBlock: 'updateBlockInContainer',
-            updateUserDataFields: 'courseware-user-data-fields/update'
-        }),
-        initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentInfo = this.info;
-            this.currentFileId = this.fileId;
-            this.currentSuccess = this.success;
-            this.currentGrade = this.grade;
-            if (this.currentFileId !== '') {
-                this.loadFile();
-            }
-        },
-        async loadFile() {
-            const id = `${this.currentFileId}`;
-            const options = { include: 'terms-of-use' };
-            await this.loadFileRef({ id: id, options });
-            const fileRef = this.fileRefById({ id: id });
-            if (fileRef && this.relatedTermOfUse({parent: fileRef, relationship: 'terms-of-use'}).attributes['download-condition'] === 0) {
-                this.updateCurrentFile({
-                    id: fileRef.id,
-                    name: fileRef.attributes.name,
-                    icon: this.getIcon(fileRef.attributes['mime-type']),
-                    download_url: this.urlHelper.getURL(
-                        'sendfile.php',
-                        { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
-                        true
-                    ),
-                });
-            }
-        },
-        updateCurrentFile(file) {
-            this.currentFile = file;
-            this.currentFileId = file.id;
-            if (!this.currentFile.icon) {
-                this.currentFile.icon = this.getIcon(file.mime_type);
-            }
-        },
-        getIcon(mimeType) {
-            let icon = 'file';
-            if (mimeType.includes('audio')) {
-                icon = 'audio';
-            }
-            if (mimeType.includes('image')) {
-                icon = 'pic';
-            }
-            if (mimeType.includes('video')) {
-                icon = 'video';
-            }
-            if (mimeType.includes('text')) {
-                icon = 'text';
-            }
-            if (mimeType.includes('pdf')) {
-                icon = 'pdf';
-            }
-            if (mimeType.includes('msword')) {
-                icon = 'word';
-            }
-            if (mimeType.includes('opendocument.text')) {
-                icon = 'word';
-            }
-            if (mimeType.includes('openxmlformats-officedocument. wordprocessingml.document')) {
-                icon = 'word';
-            }
-            if (mimeType.includes('msexcel')) {
-                icon = 'spreadsheet';
-            }
-            if (mimeType.includes('opendocument.spreadsheet')) {
-                icon = 'spreadsheet';
-            }
-            if (mimeType.includes('openxmlformats-officedocument. spreadsheetml.sheet')) {
-                icon = 'spreadsheet';
-            }
-            if (mimeType.includes('mspowerpoint')) {
-                icon = 'ppt';
-            }
-            if (mimeType.includes('zip')) {
-                icon = 'archive';
-            }
-
-            return icon;
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.title = this.currentTitle;
-            attributes.payload.info = this.currentInfo;
-            attributes.payload.success = this.currentSuccess;
-            attributes.payload.grade = this.currentGrade;
-            attributes.payload.file_id = this.currentFileId;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-        handleDownload() {
-            let data = {
-                id: this.userData.id,
-                type: 'courseware-user-data-fields',
-                relationships: {
-                    block: {
-                        data: {
-                            id: this.block.id,
-                            type: this.block.type
-                        }
-                    }
-                },
-                attributes: {
-                    payload: {
-                        downloaded: true
-                    }
-                }
-            };
-            this.updateUserDataFields(data);
-            this.userProgress = 1;
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareEmbedBlock.vue b/resources/vue/components/courseware/CoursewareEmbedBlock.vue
deleted file mode 100644
index 59457ce..0000000
--- a/resources/vue/components/courseware/CoursewareEmbedBlock.vue
+++ /dev/null
@@ -1,252 +0,0 @@
-<template>
-    <div class="cw-block cw-block-embed" ref="block">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="false"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
-                <template v-if="oembedData">
-                    <div
-                        v-if="oembedData.type === 'rich' || oembedData.type === 'video'"
-                        v-html="oembedData.html"
-                        class="cw-block-embed-iframe-wrapper"
-                        :style="{ height: contentHeight + 'px' }"
-                    ></div>
-
-                    <div v-if="oembedData.type === 'photo'" :style="{ height: contentHeight + 'px' }">
-                        <img :src="oembedData.url" />
-                    </div>
-
-                    <div v-if="oembedData.type === 'link' && oembedData.provider_name === 'DeviantArt'">
-                        <img :src="oembedData.fullsize_url" />
-                    </div>
-                    <div class="cw-block-embed-info">
-                        <span class="cw-block-embed-title">{{ oembedData.title }}</span>
-                        <span class="cw-block-embed-author-name">
-                            {{ $gettext('erstellt von') }}
-                            <a :href="oembedData.author_url" target="_blank">{{ oembedData.author_name }}</a></span
-                        >
-                        <span class="cw-block-embed-source">
-                            {{ $gettext('veröffentlicht auf') }}
-                            <a :href="oembedData.provider_url" target="_blank">{{ oembedData.provider_name }}</a></span
-                        >
-                    </div>
-                </template>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Überschrift') }}
-                        <input type="text" v-model="currentTitle" />
-                    </label>
-                    <label>
-                        {{ $gettext('Quelle') }}
-                        <select v-model="currentSource">
-                            <option v-for="(value, key) in endPoints" :key="key" :value="key">{{ key }}</option>
-                        </select>
-                    </label>
-                    <label>
-                        {{ $gettext('URL') }}
-                        <input type="text" v-model="currentUrl" />
-                    </label>
-                    <label v-if="currentSource === 'youtube'" class="col-1">
-                        {{ $gettext('Startpunkt') }}
-                        <input
-                            type="time"
-                            v-model="currentStartTime"
-                            step="1"
-                            min="00:00:00"
-                            max="24:00:00"
-                            @change="updateTime"
-                        />
-                    </label>
-                    <label v-if="currentSource === 'youtube'" class="col-1">
-                        {{ $gettext('Endpunkt') }}
-                        <input
-                            type="time"
-                            v-model="currentEndTime"
-                            step="1"
-                            :min="currentStartTime"
-                            max="24:00:00"
-                            @change="updateTime"
-                        />
-                    </label>
-                </form>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Embed-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-embed-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentSource: '',
-            currentUrl: '',
-            currentStartTime: '',
-            currentEndTime: '',
-
-            endPoints: {
-                audiomack: 'https://www.audiomack.com/oembed',
-                codepen: 'https://codepen.io/api/oembed',
-                codesandbox: 'https://codesandbox.io/oembed',
-                deviantart: 'https://backend.deviantart.com/oembed',
-                ethfiddle: 'https://ethfiddle.com/services/oembed/',
-                flickr: 'https://www.flickr.com/services/oembed/',
-                giphy: 'https://giphy.com/services/oembed',
-                kidoju: 'https://www.kidoju.com/api/oembed',
-                learningapps: 'https://learningapps.org/oembed.php',
-                sketchfab: 'https://sketchfab.com/oembed',
-                slideshare: 'https://www.slideshare.net/api/oembed/2',
-                soundcloud: 'https://soundcloud.com/oembed',
-                speakerdeck: 'https://speakerdeck.com/oembed.json',
-                sway: 'https://sway.com/api/v1.0/oembed',
-                'sway.office': 'https://sway.office.com/api/v1.0/oembed',
-                spotify: 'https://embed.spotify.com/oembed/',
-                vimeo: 'https://vimeo.com/api/oembed.json',
-                youtube: 'https://www.youtube.com/oembed',
-            },
-            oembedData: {},
-            contentHeight: 300,
-        };
-    },
-    computed: {
-        url() {
-            return this.block?.attributes?.payload?.url;
-        },
-        source() {
-            return this.block?.attributes?.payload?.source;
-        },
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        startTime() {
-            return this.block?.attributes?.payload?.starttime;
-        },
-        endTime() {
-            return this.block?.attributes?.payload?.endtime;
-        },
-        oembed() {
-            return this.block?.attributes?.payload?.oembed;
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-        window.addEventListener('resize', this.calcContentHeight);
-    },
-
-    created () {
-        STUDIP.eventBus.on('courseware:update-tab', (data) => {
-            this.recalculateContentHeight(data);
-        });
-        STUDIP.eventBus.on('courseware:update-collapsible', (data) => {
-            this.recalculateContentHeight(data);
-        });
-    },
-    destroyed() {
-        window.removeEventListener('resize', this.calcContentHeight);
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentSource = this.source;
-            this.currentUrl = this.url;
-            this.currentStartTime = this.startTime;
-            this.currentEndTime = this.endTime;
-            this.oembedData = this.oembed;
-            if (this.oembedData) {
-                this.updateTime();
-                this.calcContentHeight();
-            }
-        },
-        recalculateContentHeight(data){
-            if (this.$parent._uid === data.uid) {
-                if (this.oembedData) {
-                    this.calcContentHeight();
-                }
-            }
-        },
-        addTimeData(data) {
-            if (this.currentSource === 'youtube') {
-                if (this.currentStartTime !== '') {
-                    let start = this.currentStartTime.split(':');
-                    let s = parseInt(start[0], 10) * 3600 + parseInt(start[1], 10) * 60 + parseInt(start[2], 10);
-                    let query = '?feature=oembed&start=' + s;
-                    if (this.currentEndTime !== '') {
-                        let end = this.currentEndTime.split(':');
-                        let e = parseInt(end[0], 10) * 3600 + parseInt(end[1], 10) * 60 + parseInt(end[2], 10);
-                        query = query + '&end=' + e;
-                    }
-                    data.html = data.html.replace('?feature=oembed', query);
-                }
-            }
-            return data;
-        },
-        updateTime() {
-            this.oembedData = this.addTimeData(this.oembedData);
-        },
-        validateCurrentSource() {
-            var validSource = false;
-            let view = this;
-            for (const key of Object.keys(this.endPoints)) {
-                if (view.currentUrl.includes(key)) {
-                    view.currentSource = key;
-                    validSource = true;
-                    break;
-                }
-            }
-
-            return validSource;
-        },
-        calcContentHeight() {
-            if (this.oembedData.height && this.oembedData.width) {
-                this.contentHeight =
-                    ((this.$refs.block.offsetWidth - 4) / this.oembedData.width) * this.oembedData.height;
-            }
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.title = this.currentTitle;
-            attributes.payload.url = this.currentUrl;
-            attributes.payload.source = this.currentSource;
-            attributes.payload.starttime = this.currentStartTime;
-            attributes.payload.endtime = this.currentEndTime;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareEmptyElementBox.vue b/resources/vue/components/courseware/CoursewareEmptyElementBox.vue
deleted file mode 100644
index 4934e05..0000000
--- a/resources/vue/components/courseware/CoursewareEmptyElementBox.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<template>
-    <div class="cw-welcome-screen">
-        <courseware-companion-box :msgCompanion="this.$gettext('Es wurden bisher noch keine Inhalte eingepflegt.')">
-            <template v-slot:companionActions>
-                <button v-if="canEdit && noContainers" class="button" @click="addContainer"><translate>Einen Abschnitt hinzufügen</translate></button>
-                <button v-if="canEdit && !noContainers && !editMode" class="button" @click="switchToEditView"><translate>Seite bearbeiten</translate></button>
-            </template>
-        </courseware-companion-box>
-    </div>
-</template>
-
-<script>
-import { mapActions, mapGetters } from 'vuex';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-
-export default {
-    name: 'courseware-empty-element-box',
-    components: {
-        CoursewareCompanionBox,
-    },
-    props: {
-        canEdit: Boolean,
-        noContainers: Boolean
-    },
-    data() {
-        return{}
-    },
-    computed: {
-        ...mapGetters({
-            viewMode: 'viewMode'
-        }),
-        editMode() {
-            return this.viewMode === 'edit';
-        }
-    },
-    methods: {
-        ...mapActions({
-            coursewareViewMode: 'coursewareViewMode',
-            coursewareConsumeMode: 'coursewareConsumeMode',
-            coursewareContainerAdder: 'coursewareContainerAdder',
-            coursewareSelectedToolbarItem: 'coursewareSelectedToolbarItem',
-            coursewareShowToolbar: 'coursewareShowToolbar'
-        }),
-        addContainer() {
-            this.coursewareViewMode('edit');
-            this.coursewareConsumeMode(false);
-            this.coursewareContainerAdder(true);
-            this.coursewareSelectedToolbarItem('blockadder');
-            this.coursewareShowToolbar(true);
-        },
-        switchToEditView() {
-            this.coursewareViewMode('edit');
-            this.coursewareConsumeMode(false);
-        }
-    }
-
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareErrorBlock.vue b/resources/vue/components/courseware/CoursewareErrorBlock.vue
deleted file mode 100644
index f0eb1f6..0000000
--- a/resources/vue/components/courseware/CoursewareErrorBlock.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<template>
-    <div class="cw-block cw-block-error">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :deleteOnly="true"
-            :isTeacher="isTeacher"
-            :preview="false"
-            :defaultGrade="false"
-        >
-            <template #content>
-                <div class="cw-block-error-content">
-                    <courseware-companion-box 
-                        mood="sad"
-                        :msgCompanion="errorMessage"
-                    >
-                    </courseware-companion-box>
-                </div>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { blockMixin } from './block-mixin.js';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-
-export default {
-    name: 'courseware-error-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareCompanionBox,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    computed: {
-        originalBlockType() {
-            return this.block?.attributes?.payload?.original_block_type;
-        },
-        errorMessage() {
-            let message = '<b>'
-            message += this.$gettext('Es ist ein Fehler aufgetretten! Der Block-Typ dieses Blocks ist nicht verfügbar.');
-            message += '</b><br>'
-            message += 'block_type: ' + this.originalBlockType + ' not found';
-
-            return message;
-        }
-    },
-
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareExportWidget.vue b/resources/vue/components/courseware/CoursewareExportWidget.vue
deleted file mode 100644
index a6473ca..0000000
--- a/resources/vue/components/courseware/CoursewareExportWidget.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<template>
-    <sidebar-widget :title="$gettext('Export')" v-if="structuralElement">
-        <template #content>
-            <ul class="widget-list widget-links cw-export-widget" v-if="structuralElement">
-                <li v-if="showExportArchiv" class="cw-export-widget-export">
-                    <button @click="exportElement">
-                        {{ $gettext('Seiten exportieren') }}
-                    </button>
-                </li>
-                <li v-if="showExportPdf" class="cw-export-widget-export-pdf">
-                    <button @click="pdfElement">
-                        {{ $gettext('PDF-Dokument erstellen') }}
-                    </button>
-                </li>
-                <li v-if="showOer" class="cw-export-widget-oer">
-                    <button @click="oerElement">
-                        {{ $gettext('Auf OER Campus veröffentlichen') }}
-                    </button>
-                </li>
-                <li v-if="!showExportArchiv && !showExportPdf && !showOer">
-                    {{ $gettext('Keine Exportoptionen verfügbar') }}
-                </li>
-            </ul>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-import CoursewareExport from '@/vue/mixins/courseware/export.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-export-widget',
-    props: ['structuralElement', 'canVisit'],
-    components: {
-        SidebarWidget
-    },
-    mixins: [CoursewareExport],
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            oerCampusEnabled: 'oerCampusEnabled',
-            userIsTeacher: 'userIsTeacher',
-        }),
-        canEdit() {
-            if (!this.structuralElement) {
-                return false;
-            }
-            return this.structuralElement.attributes['can-edit'];
-        },
-        showExportArchiv() {
-            if (this.context.type === 'users') {
-                return true;
-            }
-
-            return this.canEdit;
-        },
-        showExportPdf() {
-            if (this.context.type === 'users') {
-                return true;
-            }
-
-            return this.canVisit;
-        },
-        showOer() {
-            if (!this.oerCampusEnabled) {
-                return false;
-            }
-
-            if (this.context.type === 'users') {
-                return true;
-            }
-
-            return this.userIsTeacher && this.canVisit;
-        }
-    },
-    methods: {
-        ...mapActions({
-            showElementExportDialog: 'showElementExportDialog',
-            showElementPdfExportDialog: 'showElementPdfExportDialog',
-            showElementOerDialog: 'showElementOerDialog',
-        }),
-        exportElement() {
-            this.showElementExportDialog(true);
-        },
-        pdfElement() {
-            this.showElementPdfExportDialog(true);
-        },
-        oerElement() {
-            this.showElementOerDialog(true);
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareFileChooser.vue b/resources/vue/components/courseware/CoursewareFileChooser.vue
deleted file mode 100644
index 6571b34..0000000
--- a/resources/vue/components/courseware/CoursewareFileChooser.vue
+++ /dev/null
@@ -1,139 +0,0 @@
-<template>
-    <div class="cw-file-chooser">
-        <span v-translate>Ordner-Filter</span>
-        <courseware-folder-chooser allowUserFolders unchoose v-model="selectedFolderId" />
-        <span v-translate>Datei</span>
-        <select v-model="currentValue" @change="selectFile">
-            <option v-show="canBeEmpty" value="">
-                <translate>Keine Auswahl</translate>
-            </option>
-            <option v-for="(file, index) in files" :key="index" :value="file.id">
-                {{ file.name }}
-            </option>
-            <option v-show="files.length === 0" disabled>
-                <translate>Keine Dateien vorhanden</translate>
-            </option>
-        </select>
-    </div>
-</template>
-
-<script>
-import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
-
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-file-chooser',
-    components: { CoursewareFolderChooser },
-    props: {
-        value: String,
-        mimeType: { type: String, default: '' },
-        isImage: { type: Boolean, default: false },
-        isVideo: { type: Boolean, default: false },
-        isAudio: { type: Boolean, default: false },
-        isDocument: { type: Boolean, default: false },
-        canBeEmpty: { type: Boolean, default: false },
-    },
-    data() {
-        return {
-            currentValue: '',
-            selectedFolderId: '',
-            files: [],
-        };
-    },
-    computed: {
-        ...mapGetters({
-            fileRefById: 'file-refs/byId',
-            relatedFileRefs: 'file-refs/related',
-            urlHelper: 'urlHelper',
-            userId: 'userId',
-            relatedTermOfUse: 'terms-of-use/related'
-        }),
-    },
-    methods: {
-        ...mapActions({
-            loadFileRef: 'file-refs/loadById',
-            loadRelatedFileRefs: 'file-refs/loadRelated',
-        }),
-        selectFile() {
-            this.$emit(
-                'selectFile',
-                this.files.find((file) => file.id === this.currentValue)
-            );
-        },
-        filterFiles(loadArray) {
-            const filterFile = (file) => {
-                let fileTermsOfUse = this.relatedTermOfUse({parent: file, relationship: 'terms-of-use'});
-
-                if (fileTermsOfUse !== null && fileTermsOfUse.attributes['download-condition'] !== 0) {
-                    return false;
-                }
-                if (this.mimeType !== '' && this.mimeType !== file.attributes['mime-type']) {
-                    return false;
-                }
-                if (this.isImage && !file.attributes['mime-type'].includes('image')) {
-                    return false;
-                }
-                const videoConditions = ['video/mp4', 'video/ogg', 'video/webm'];
-                if (this.isVideo && !videoConditions.some(condition => file.attributes['mime-type'].includes(condition))) {
-                    return false;
-                }
-                const audioConditions = ['audio/wav', 'audio/ogg', 'audio/webm','audio/flac', 'audio/mpeg', 'audio/x-m4a', 'audio/mp4'];
-                if (this.isAudio && !audioConditions.some(condition => file.attributes['mime-type'].includes(condition)) ) {
-                    return false;
-                }
-                const officeConditions = ['application/pdf']; //TODO enable more mime types
-                if (this.isDocument && !officeConditions.some(condition => file.attributes['mime-type'].includes(condition)) ) {
-                    return false;
-                }
-
-                return true;
-            };
-
-            return loadArray.filter(filterFile).map((file) => ({
-                id: file.id,
-                name: file.attributes.name,
-                mime_type: file.attributes['mime-type'],
-                download_url: this.urlHelper.getURL(
-                    'sendfile.php',
-                    { type: 0, file_id: file.id, file_name: file.attributes.name },
-                    true
-                ),
-            }));
-        },
-        async getFolderFiles() {
-            const parent = { type: 'folders', id: `${this.selectedFolderId}` };
-            const relationship = 'file-refs';
-            const options = { include: 'terms-of-use', 'page[limit]': 10000 };
-            await this.loadRelatedFileRefs({ parent, relationship, options });
-
-            const files = this.relatedFileRefs({ parent, relationship });
-            this.files = this.filterFiles(files);
-        },
-    },
-    async mounted() {
-        if (this.value != '') {
-            await this.loadFileRef({ id: this.value });
-            const fileRef = this.fileRefById({ id: this.value });
-
-            if (fileRef) {
-                this.selectedFolderId = fileRef.relationships.parent.data.id;
-                this.currentValue = this.value;
-            }
-        }
-    },
-    watch: {
-        selectedFolderId() {
-            if (this.selectedFolderId !== '') {
-                this.getFolderFiles();
-            }
-        },
-        value(newValue, oldValue) {
-            if (newValue === '') {
-                this.selectedFolderId = '';
-                this.currentValue = '';
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareFolderBlock.vue b/resources/vue/components/courseware/CoursewareFolderBlock.vue
deleted file mode 100644
index 2090c35..0000000
--- a/resources/vue/components/courseware/CoursewareFolderBlock.vue
+++ /dev/null
@@ -1,364 +0,0 @@
-<template>
-    <div class="cw-block cw-block-folder">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
-                <div v-if="isHomework" class="cw-block-folder-info">
-                    <p>
-                        {{ $gettext('Dieser Ordner ist ein Hausaufgabenordner. Es können nur Dateien eingestellt werden.') }}
-                    </p>
-                    <p v-if="!isTeacher">
-                        {{ $gettext('Sie selbst haben folgende Dateien in diesen Ordner eingestellt') }}:
-                    </p>
-                </div>
-                <ul class="cw-block-folder-list">
-                    <li v-for="file in files" :key="file.id" class="cw-block-folder-file-item">
-                        <a
-                            v-if="downloadEnabled"
-                            target="_blank"
-                            :download="file.attributes.name"
-                            :title="$gettext('Datei herunterladen')"
-                            :href="file.meta['download-url']"
-                        >
-                            <span class="cw-block-file-info" :class="['cw-block-file-icon-' + getIcon(file.attributes['mime-type'])]">
-                                {{ file.attributes.name }}
-                            </span>
-                            <div v-if="isTeacher && isHomework" class="cw-block-file-details">
-                                <span class="cw-block-file-owner">
-                                    {{ file.relationships.owner.meta.name }}
-                                </span>
-                                <span class="cw-block-file-mkdate">
-                                    {{ getFormattedDate(file.attributes.mkdate) }}
-                                </span>
-                            </div>
-                        </a>
-                        <template v-else>
-                            <span class="cw-block-file-info download-disabled" :class="['cw-block-file-icon-' + getIcon(file.attributes['mime-type'])]">
-                                {{ file.attributes.name }}
-                            </span>
-                            <div class="cw-block-file-details">
-                                <span class="cw-block-file-mkdate">
-                                    {{ getFormattedDate(file.attributes.mkdate) }}
-                                </span>
-                            </div>
-                        </template>
-                    </li>
-                    <li v-if="files.length === 0">
-                        <span class="cw-block-file-info cw-block-file-icon-empty">
-                            {{ $gettext('Dieser Ordner ist leer') }}
-                        </span>
-                    </li>
-                </ul>
-                <div v-if="uploadEnabled" class="cw-block-folder-upload">
-                    <form class="default" @submit.prevent="">
-                        <label>
-                            {{ $gettext('Dateien zum Hochladen auswählen') }}
-                            <input class="cw-file-input" ref="uploadFile" type="file" @change="displayTermSelector"/>
-                            <button class="button" @click="uploadFile">
-                                {{ $gettext('Datei hochladen') }}
-                            </button>
-                        </label>
-                    </form>
-                    <studip-dialog
-                        v-if="showTermSelector"
-                        width="780"
-                        height="510"
-                        :title="$gettext('Lizenz auswählen')"
-                        :confirmText="$gettext('Speichern')"
-                        confirmClass="accept"
-                        :closeText="$gettext('Lizenzauswahl abbrechen')"
-                        closeClass="cancel"
-                        @close="showTermSelector = false"
-                        @confirm="selectTerm"
-                    >
-                        <template v-slot:dialogContent>
-                            <form class="default" @submit.prevent="">
-                                <div style="margin-bottom: 1ex;">
-                                    {{ $gettext('Bereitgestellte Dateien können heruntergeladen und ggf. weiterverbreitet werden. Dabei ist das Urheberrecht sowohl beim Hochladen der Datei als auch bei der Nutzung zu beachten. Bitte geben Sie daher an, um welche Art von Bereitstellung es sich handelt. Diese Angabe dient mehreren Zwecken: Beim Herunterladen wird ein Hinweis angezeigt, welche Nutzung der Datei zulässig ist. Beim Hochladen stellt die Angabe eine Entscheidungshilfe dar, damit Sie sichergehen können, dass die Datei tatsächlich bereitgestellt werden darf.') }}
-                                </div>
-                                <fieldset class="select_terms_of_use">
-                                    <template v-for="term in termsOfUse">
-                                        <input
-                                            type="radio"
-                                            name="content_terms_of_use_id"
-                                            :value="term.id"
-                                            v-model="selectedTerm"
-                                            :id="'content_terms_of_use-' + term.id"
-                                            :checked="selectedTerm === term.id"
-                                            :aria-description="term.description"
-                                            :key="term.id + '_input'"
-                                        />
-                                        <label @click="selectedTerm = term.id" :key="term.id + 'label'">
-                                            <div class="icon">
-                                                <studip-icon :shape="term.attributes.icon" size="32"/>
-                                            </div>
-                                            <div class="text">
-                                                {{ term.attributes.name }}
-                                            </div>
-                                            <studip-icon shape="arr_1down" size="24" class="arrow" />
-                                            <studip-icon shape="check-circle" size="24" class="check" />
-                                        </label>
-                                        <div class="terms_of_use_description" :key="term.id + '_description'">
-                                            <div class="description">
-                                                {{ term.attributes.description }}
-                                            </div>
-                                        </div>
-                                    </template>
-                                </fieldset>
-                            </form>
-                        </template>
-                    </studip-dialog>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Überschrift') }}
-                        <input type="text" v-model="currentTitle" />
-                    </label>
-                    <label>
-                        {{ $gettext('Ordner') }}
-                        <courseware-folder-chooser v-model="currentFolderId" allowUserFolders allowHomeworkFolders />
-                    </label>
-                </form>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Dateiordner-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
-import StudipDialog from '../StudipDialog.vue';
-
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-folder-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareFolderChooser,
-        StudipDialog
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentFolderId: '',
-            currentFolderType: '',
-            showTermSelector: false,
-            selectedTerm: null,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            folderById: 'folders/byId',
-            termsOfUse: 'terms-of-use/all'
-        }),
-        folderType() {
-            return this.block?.attributes?.payload?.type;
-        },
-        storedFolderType() {
-            return this.block?.attributes?.payload?.folder_type;
-        },
-        folderTypeHasChanged() {
-            return this.folderType === this.storedFolderType;
-        },
-        folderId() {
-            return this.block?.attributes?.payload?.folder_id;
-        },
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        files() {
-            return this.block?.attributes?.payload?.files;
-        },
-        isHomework() {
-            return this.folderType === 'HomeworkFolder';
-        },
-        uploadEnabled() {
-            return !this.isTeacher && this.isHomework;
-        },
-        downloadEnabled() {
-            return this.isTeacher || !this.isHomework;
-        }
-    },
-    async mounted() {
-        await this.loadTermsOfUse();
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            loadFolder: 'folders/loadById',
-            loadBlock: 'courseware-blocks/loadById',
-            updateBlock: 'updateBlockInContainer',
-            createFile: 'createFile',
-            companionWarning: 'companionWarning',
-            companionSuccess: 'companionSuccess',
-            companionError: 'companionError',
-            loadTermsOfUse: 'terms-of-use/loadAll'
-        }),
-        async initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentFolderId = this.folderId;
-            if (this.$refs?.uploadFile) {
-                this.$refs.uploadFile.value = null;
-            }
-            this.selectedTerm = this.getDefaultTerm();
-        },
-        async setCurrentFolderType() {
-            await this.loadFolder({ id: this.currentFolderId });
-            const folder = this.folderById({ id: this.currentFolderId });
-            this.currentFolderType = folder?.attributes['folder-type'];
-        },
-        getIcon(mimeType) {
-            let icon = 'file';
-            if (mimeType.includes('audio')) {
-                icon = 'audio';
-            }
-            if (mimeType.includes('image')) {
-                icon = 'pic';
-            }
-            if (mimeType.includes('video')) {
-                icon = 'video';
-            }
-            if (mimeType.includes('text')) {
-                icon = 'text';
-            }
-            if (mimeType.includes('pdf')) {
-                icon = 'pdf';
-            }
-            if (mimeType.includes('msword')) {
-                icon = 'word';
-            }
-            if (mimeType.includes('opendocument.text')) {
-                icon = 'word';
-            }
-            if (mimeType.includes('openxmlformats-officedocument. wordprocessingml.document')) {
-                icon = 'word';
-            }
-            if (mimeType.includes('msexcel')) {
-                icon = 'spreadsheet';
-            }
-            if (mimeType.includes('opendocument.spreadsheet')) {
-                icon = 'spreadsheet';
-            }
-            if (mimeType.includes('openxmlformats-officedocument. spreadsheetml.sheet')) {
-                icon = 'spreadsheet';
-            }
-            if (mimeType.includes('mspowerpoint')) {
-                icon = 'ppt';
-            }
-            if (mimeType.includes('zip')) {
-                icon = 'archive';
-            }
-
-            return icon;
-        },
-        getFormattedDate(unformattedDate) {
-            const date = new Date(unformattedDate);
-            const localeDate = date.toLocaleDateString("de-DE", {
-                year: "numeric",
-                month: "2-digit",
-                day: "2-digit",
-            });
-
-            return `${localeDate} ${date.getHours()}:${(date.getMinutes() < 10 ? '0' : '') + date.getMinutes()}:${(date.getSeconds() < 10 ? '0' : '') + date.getSeconds()}`;
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.title = this.currentTitle;
-            attributes.payload.folder_id = this.currentFolderId;
-            attributes.payload.type = this.currentFolderType;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-        displayTermSelector() {
-            this.showTermSelector = true;
-        },
-        selectTerm() {
-            this.showTermSelector = false;
-            this.uploadFile();
-        },
-        async uploadFile() {
-            const userFile = this.$refs?.uploadFile?.files[0];
-            if (!userFile) {
-                this.companionWarning({
-                    info: this.$gettext('Bitte wählen Sie eine Datei aus.')
-                });
-                return;
-            }
-
-            let file = {
-                attributes: {
-                    name: userFile.name.replace(/\s/g, '_')
-                },
-                relationships: {
-                    'terms-of-use': {
-                        data: {
-                            id: this.selectedTerm
-                        }
-                    }
-                }
-            };
-            let fileObj = await this.createFile({
-                file: file,
-                filedata: userFile,
-                folder: { id: this.currentFolderId }
-            });
-            if (fileObj && fileObj.type === 'file-refs') {
-                this.companionSuccess({
-                    info: this.$gettext('Die Datei wurde erfolgreich im Dateibereich abgelegt.')
-                });
-            } else {
-                if (this.folderType !== 'HomeworkFolder') {
-                    this.companionError({
-                        info: this.$gettext('Es ist ein Fehler aufgetretten.')
-                    });
-                }
-            }
-            this.reload();
-        },
-        async reload() {
-            await this.loadBlock({ id: this.block.id });
-            this.initCurrentData();
-        },
-        getDefaultTerm() {
-            const defaultTerm = this.termsOfUse.filter(term => term.attributes['is-default'])[0];
-            if (defaultTerm) {
-                return defaultTerm.id;
-            }
-            return null;
-        }
-    },
-    watch: {
-        currentFolderId() {
-            if (this.canEdit) {
-                this.setCurrentFolderType();
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareFolderChooser.vue b/resources/vue/components/courseware/CoursewareFolderChooser.vue
deleted file mode 100644
index e071679..0000000
--- a/resources/vue/components/courseware/CoursewareFolderChooser.vue
+++ /dev/null
@@ -1,167 +0,0 @@
-<template>
-    <select v-model="currentValue" @change="changeSelection">
-        <option v-if="unchoose" value=""><translate>kein Ordner ausgewählt</translate></option>
-        <optgroup v-if="this.context.type === 'courses'" :label="textOptGroupCourse">
-            <option v-for="folder in loadedCourseFolders" :key="folder.id" :value="folder.id">
-                {{ folder.attributes.name }}
-            </option>
-        </optgroup>
-        <optgroup v-if="allowUserFolders" :label="textOptGroupUser">
-            <option v-for="folder in loadedUserFolders" :key="folder.id" :value="folder.id">
-                {{ folder.attributes.name }}
-            </option>
-        </optgroup>
-    </select>
-</template>
-
-<script>
-import { mapActions, mapGetters } from 'vuex';
-
-function filterCourseFolders(folders, { allowHomeworkFolders }) {
-    const validatedParents = new Map();
-
-    return folders.filter((folder) => {
-        if (validateParentFolder(folder)) {
-            switch (folder.attributes['folder-type']) {
-                case 'HiddenFolder':
-                    if (folder.attributes['data-content']['download_allowed'] === 1) {
-                        return true;
-                    }
-                    break;
-                case 'HomeworkFolder':
-                    if (allowHomeworkFolders) {
-                        return true;
-                    }
-                    break;
-                default:
-                    return true;
-            }
-        }
-    });
-
-    function validateParentFolder(folder) {
-        let isValid = true;
-        if (folder?.relationships?.parent) {
-            let parentId = folder.relationships.parent.data.id;
-            if (validatedParents.has(parentId)) {
-                isValid = validatedParents.get(parentId);
-            } else {
-                let parent = folders.find((f) => f.id === parentId);
-                if (parent) {
-                    isValid = hiddenParentFolderValidation(parent);
-                    validatedParents.set(parentId, isValid);
-                }
-            }
-        }
-        return isValid;
-    }
-
-    function hiddenParentFolderValidation(parentFolder) {
-        if (parentFolder.attributes['folder-type'] === 'HiddenFolder') {
-            return false;
-        } else if (parentFolder?.relationships?.parent) {
-            // Recursively validating the parents.
-            return validateParentFolder(parentFolder);
-        } else {
-            return true;
-        }
-    }
-}
-export default {
-    name: 'courseware-folder-chooser',
-    props: {
-        value: String,
-        allowUserFolders: { type: Boolean, default: false },
-        allowHomeworkFolders: { type: Boolean, default: false },
-        unchoose: { type: Boolean, default: false },
-    },
-    data() {
-        return {
-            currentValue: Object,
-            textOptGroupCourse: this.$gettext('Dateibereich dieser Veranstaltung'),
-            textOptGroupUser: this.$gettext('Persönlicher Dateibereich'),
-        };
-    },
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            relatedFolders: 'folders/related',
-            userId: 'userId',
-        }),
-        courseObject() {
-            return { type: 'courses', id: `${this.context.id}` };
-        },
-        userObject() {
-            return { type: 'users', id: `${this.userId}` };
-        },
-        loadedCourseFolders() {
-            return filterCourseFolders(
-                this.relatedFolders({ parent: this.courseObject, relationship: 'folders' }) ?? [],
-                {
-                    allowHomeworkFolders: this.allowHomeworkFolders,
-                }
-            );
-        },
-        loadedUserFolders() {
-            let loadedUserFolders = [];
-            let UserFolders = this.relatedFolders({ parent: this.userObject, relationship: 'folders' }) ?? [];
-            UserFolders.forEach((folder) => {
-                if (folder.attributes['folder-type'] === 'PublicFolder') {
-                    loadedUserFolders.push(folder);
-                }
-            });
-
-            return loadedUserFolders;
-        },
-    },
-    methods: {
-        ...mapActions({
-            loadRelatedFolders: 'folders/loadRelated',
-        }),
-
-        changeSelection() {
-            this.$emit('input', this.currentValue);
-        },
-
-        getCourseFolders() {
-            const parent = this.courseObject;
-            const relationship = 'folders';
-            const options = { 'page[limit]': 10000 };
-
-            return this.loadRelatedFolders({ parent, relationship, options });
-        },
-
-        getUserFolders() {
-            const parent = this.userObject;
-            const relationship = 'folders';
-            const options = { 'page[limit]': 10000 };
-
-            return this.loadRelatedFolders({ parent, relationship, options });
-        },
-
-        confirmSelectedFolder() {
-            const folders = this.loadedUserFolders.concat(this.loadedCourseFolders);
-
-            let folder = folders.find( folder => folder.id === this.currentValue);
-
-            if (this.currentValue !== '' && folder === undefined) {
-                this.currentValue = '';
-                this.changeSelection();
-            }
-        }
-    },
-    async mounted() {
-        this.currentValue = this.value;
-        if (this.context.type !== 'users') {
-            await this.getCourseFolders();
-        }
-        await this.getUserFolders();
-        this.confirmSelectedFolder();
-    },
-    watch: {
-        value() {
-            this.currentValue = this.value;
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareGalleryBlock.vue b/resources/vue/components/courseware/CoursewareGalleryBlock.vue
deleted file mode 100644
index 29ba44f..0000000
--- a/resources/vue/components/courseware/CoursewareGalleryBlock.vue
+++ /dev/null
@@ -1,395 +0,0 @@
-<template>
-    <div class="cw-block cw-block-gallery">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="showEdit"
-            @storeEdit="storeBlock"
-            @closeEdit="closeEdit"
-        >
-            <template #content>
-                <template v-if="files.length !== 0">
-                    <div v-if="currentLayout === 'carousel'" class="cw-block-gallery-content" :style="{ height: `${currentHeight}px` }">
-                        <div
-                            v-for="(image, index) in files"
-                            :key="image.id"
-                            ref="images"
-                            class="cw-block-gallery-slides cw-block-gallery-fade"
-                        >
-                            <div class="cw-block-gallery-number-text">{{ index + 1 }} / {{ files.length }}</div>
-                            <img
-                                :src="image.meta['download-url']"
-                                :style="{ height: `${currentHeight}px` }"
-                                @load="
-                                    if (files.length - 1 === index) {
-                                        startGallery();
-                                    }
-                                "
-                            />
-                            <div class="cw-block-gallery-file-description"
-                                :class="{'show-on-hover': currentMouseoverFileNames === 'true'}">
-                                <p v-if="currentShowFileNames === 'true'">{{ image?.attributes?.name }}</p>
-                                <p v-if="currentShowFileDescription === 'true'">{{ image?.attributes?.description }}</p>
-                            </div>
-                        </div>
-                        <div v-if="currentNav === 'true'">
-                            <button class="cw-block-gallery-prev" :title="$gettext('Vorheriges Bild')" @click="plusSlides(-1)"></button>
-                            <button class="cw-block-gallery-next" :title="$gettext('Nächstes Bild')" @click="plusSlides(1)"></button>
-                        </div>
-                    </div>
-                    <div v-if="currentLayout === 'grid'" class="cw-block-gallery-content">
-                        <div class="cw-block-gallery-grid">
-                            <figure
-                                v-for="image in files"
-                                :key="image.id"
-                                :style="{ 'max-width': gridWidth }"
-                            >
-                                <img :src="image.meta['download-url']" :title="image?.attributes?.name"/>
-                                <figcaption v-if="showDescription">
-                                    <p v-if="currentShowFileNames === 'true'" class="cw-block-gallery-grid-file-name">
-                                        {{ image?.attributes?.name }}
-                                    </p>
-                                    <p v-if="currentShowFileDescription === 'true'" class="cw-block-gallery-grid-file-description">
-                                        {{ image?.attributes?.description }}
-                                    </p>
-                                </figcaption>
-                            </figure>
-                        </div>
-                    </div>
-                </template>
-            </template>
-            <template v-if="canEdit" #edit>
-                <courseware-tabs>
-                    <courseware-tab
-                        :index="0"
-                        :name="$gettext('Einstellungen')"
-                        :selected="true"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Layout') }}
-                                <select v-model="currentLayout">
-                                    <option value="carousel">{{ $gettext('Karussell') }}</option>
-                                    <option value="grid">{{ $gettext('Gitter') }}</option>
-                                </select>
-                            </label>
-                            <label>
-                                {{ $gettext('Ordner') }}
-                                <courseware-folder-chooser v-model="currentFolderId" allowUserFolders />
-                            </label>
-                            <label v-if="currentLayout === 'carousel'">
-                                {{ $gettext('Höhe') }}
-                                <input type="number" min="0" max="800" v-model="currentHeight" />
-                            </label>
-                            <label v-if="currentLayout === 'grid'">
-                                {{ $gettext('Gitter-Spalten') }}
-                                <select v-model="currentCols">
-                                    <option :value="100">1</option>
-                                    <option :value="50">2</option>
-                                    <option :value="33">3</option>
-                                    <option :value="25">4</option>
-                                    <option :value="20">5</option>
-                                </select>
-                            </label>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="1"
-                        :name="$gettext('Beschreibung')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Dateinamen anzeigen') }}
-                                <select v-model="currentShowFileNames">
-                                    <option value="true">{{ $gettext('Ja') }}</option>
-                                    <option value="false">{{ $gettext('Nein') }}</option>
-                                </select>
-                            </label>
-                            <label>
-                                {{ $gettext('Dateibeschreibung anzeigen') }}
-                                <select v-model="currentShowFileDescription">
-                                    <option value="true">{{ $gettext('Ja') }}</option>
-                                    <option value="false">{{ $gettext('Nein') }}</option>
-                                </select>
-                            </label>
-                            <label v-if="showDescription && currentLayout === 'carousel'">
-                                {{ $gettext('Beschreibung erscheint bei Mouseover') }}
-                                <studip-tooltip-icon
-                                    :text="$gettext('Der Beschreibungstext wird angezeigt, wenn Sie den Mauszeiger über das Bild bewegen.')"/>
-                                <select v-model="currentMouseoverFileNames">
-                                    <option value="true">{{ $gettext('Ja') }}</option>
-                                    <option value="false">{{ $gettext('Nein') }}</option>
-                                </select>
-                            </label>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        v-if="currentLayout === 'carousel'"
-                        :index="2"
-                        :name="$gettext('Autoplay')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Autoplay') }}
-                                <select v-model="currentAutoplay">
-                                    <option value="true">{{ $gettext('Ja') }}</option>
-                                    <option value="false">{{ $gettext('Nein') }}</option>
-                                </select>
-                            </label>
-                            <label v-if="currentAutoplay === 'true'">
-                                {{ $gettext('Autoplay Timer in Sekunden') }}
-                                <input type="number" min="1" max="60" v-model="currentAutoplayTimer" />
-                            </label>
-                            <label v-if="currentAutoplay === 'true'">
-                                {{ $gettext('Navigation') }}
-                                <select v-model="currentNav">
-                                    <option value="true">{{ $gettext('Ja') }}</option>
-                                    <option value="false">{{ $gettext('Nein') }}</option>
-                                </select>
-                            </label>
-                        </form>
-                    </courseware-tab>
-
-                </courseware-tabs>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Galerie-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-gallery-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareFolderChooser,
-        CoursewareTabs,
-        CoursewareTab,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentFolderId: '',
-            currentLayout: '',
-            currentAutoplay: '',
-            currentNav: '',
-            currentHeight: '',
-            currentCols: 33,
-            currentShowFileNames: '',
-            currentShowFileDescription: '',
-            currentMouseoverFileNames: '',
-            currentAutoplayTimer: '',
-            editModeFiles: [],
-            slideIndex: 0,
-            editMode: false,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            urlHelper: 'urlHelper',
-            relatedFileRefs: 'file-refs/related',
-            relatedTermOfUse: 'terms-of-use/related',
-        }),
-        folderId() {
-            return this.block?.attributes?.payload?.folder_id;
-        },
-        layout() {
-            return this.block?.attributes?.payload?.layout ?? 'carousel';
-        },
-        autoplay() {
-            return this.block?.attributes?.payload?.autoplay;
-        },
-        autoplayTimer() {
-            return this.block?.attributes?.payload?.autoplay_timer;
-        },
-        nav() {
-            return this.block?.attributes?.payload?.nav;
-        },
-        height() {
-            return this.block?.attributes?.payload?.height;
-        },
-        cols() {
-            return this.block?.attributes?.payload?.cols;
-        },
-        showFileNames() {
-            return this.block?.attributes?.payload?.show_filenames;
-        },
-        showFileDescription() {
-            return this.block?.attributes?.payload?.show_description ?? 'false';
-        },
-        mouseoverFileNames() {
-            return this.block?.attributes?.payload?.mouseover_filenames ?? 'false';
-        },
-        files() {
-            if (!this.editMode) {
-                return this.block?.attributes?.payload?.files;
-            }
-            return this.editModeFiles;
-        },
-        gridWidth() {
-            return 'calc(' + this.currentCols + '% - 8px)';
-        },
-        showDescription() {
-            return this.currentShowFileNames === 'true' || this.currentShowFileDescription === 'true';
-        }
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            loadRelatedFileRefs: 'file-refs/loadRelated',
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            this.currentFolderId = this.folderId;
-            this.currentLayout = this.layout;
-            this.currentAutoplay = this.autoplay;
-            this.currentAutoplayTimer = this.autoplayTimer;
-            this.currentNav = this.nav;
-            this.currentHeight = this.height;
-            this.currentCols = this.cols;
-            this.currentShowFileNames = this.showFileNames;
-            this.currentShowFileDescription = this.showFileDescription;
-            this.currentMouseoverFileNames = this.mouseoverFileNames;
-        },
-        startGallery() {
-            this.slideIndex = 0;
-            this.showSlides(0);
-            if (this.currentAutoplay === 'true') {
-                this.playSlides();
-            }
-        },
-        async getFolderFiles() {
-            const parent = { type: 'folders', id: `${this.currentFolderId}` };
-            const relationship = 'file-refs';
-            const options = { include: 'terms-of-use'}
-            await this.loadRelatedFileRefs({ parent, relationship, options });
-
-            const files = this.relatedFileRefs({ parent, relationship });
-            this.processFiles(files);
-        },
-        processFiles(files) {
-            this.editModeFiles = files
-                .filter((file) => {
-                    if (this.relatedTermOfUse({parent: file, relationship: 'terms-of-use'}).attributes['download-condition'] !== 0) {
-                        return false;
-                    }
-                    if (! file.attributes['mime-type'].includes('image')) {
-                        return false;
-                    }
-
-                    return true;
-                })
-                .map((file) => ({
-                    id: file.id,
-                    attributes: {
-                        name: file.attributes.name,
-                        description: file.attributes.description
-                    },
-                    meta: {
-                        'download-url': this.urlHelper.getURL(
-                            'sendfile.php',
-                            { type: 0, file_id: file.id, file_name: file.attributes.name },
-                            true
-                        ),
-                    },
-                }));
-        },
-        showEdit() {
-            this.editMode = true;
-            this.initCurrentData();
-        },
-        closeEdit() {
-            this.editMode = false;
-            this.initCurrentData();
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.folder_id = this.currentFolderId;
-            attributes.payload.layout = this.currentLayout;
-            attributes.payload.autoplay = this.currentAutoplay;
-            attributes.payload.autoplay_timer = this.currentAutoplayTimer;
-            attributes.payload.nav = this.currentNav;
-            attributes.payload.height = this.currentHeight;
-            attributes.payload.cols = this.currentCols;
-            attributes.payload.show_filenames = this.currentShowFileNames;
-            attributes.payload.show_description = this.currentShowFileDescription;
-            attributes.payload.mouseover_filenames = this.currentMouseoverFileNames;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-        plusSlides(n) {
-            this.showSlides((this.slideIndex += n));
-        },
-        showSlides(n) {
-            let slides = this.$refs.images;
-            if (slides === undefined) {
-                return false;
-            }
-            if (n > slides.length - 1) {
-                this.slideIndex = 0;
-            }
-            if (n < 0) {
-                this.slideIndex = slides.length - 1;
-            }
-            slides.forEach((slide) => {
-                slide.style.display = 'none';
-            });
-            slides[this.slideIndex].style.display = 'block';
-        },
-        playSlides() {
-            let slides = this.$refs.images;
-            slides.forEach((slide) => {
-                slide.style.display = 'none';
-            });
-            if (this.slideIndex > slides.length - 1) {
-                this.slideIndex = 0;
-            }
-            if (slides[this.slideIndex]) {
-                slides[this.slideIndex].style.display = 'block';
-            }
-            this.slideIndex++;
-            if (this.currentAutoplay === 'true') {
-                setTimeout(this.playSlides, this.currentAutoplayTimer * 1000);
-            }
-        },
-    },
-    watch: {
-        currentFolderId() {
-            this.getFolderFiles();
-        },
-        currentAutoplay(value) {
-            if (value === 'false') {
-                this.currentNav = 'true';
-            }
-        },
-        currentAutoplayTimer(value) {
-            if (value > 60 || value < 1) {
-                this.currentAutoplayTimer = '2';
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareHeadlineBlock.vue b/resources/vue/components/courseware/CoursewareHeadlineBlock.vue
deleted file mode 100644
index c993300..0000000
--- a/resources/vue/components/courseware/CoursewareHeadlineBlock.vue
+++ /dev/null
@@ -1,634 +0,0 @@
-<template>
-    <div class="cw-block cw-block-headline">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeText"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div
-                    class="cw-block-headline-content"
-                    :class="[
-                        currentStyle,
-                        currentHeight,
-                        hasGradient ? currentGradient : ''
-                    ]"
-                    :style="headlineStyle"
-                >
-                    <div
-                        class="cw-block-headline-iconbox"
-                        :class="['border-' + currentIconColor, currentHeight]"
-                    >
-                        <div
-                            class="icon-layer"
-                            :class="['icon-' + currentIconColor + '-' + currentIcon, currentHeight]"
-                        >
-                        </div>
-                    </div>
-                    <div
-                        class="cw-block-headline-textbox"
-                        :class="['border-' + currentIconColor, currentHeight]"
-                        :style="currentStyle === 'ribbon' ? headlineTextboxStyle.rgba : {}"
-                    >
-                        <div class="cw-block-headline-title" :style="currentStyle === 'vertical' ? headlineTextboxStyle.hex : {}">
-                            <h1 :style="textStyle">{{ currentTitle }}</h1>
-                        </div>
-                        <div v-show="hasSubtitle && subtitleIsSet" class="cw-block-headline-subtitle" :style="currentStyle === 'vertical' ? headlineTextboxStyle.rgba : {}">
-                            <h2 :style="textStyle">{{ currentSubtitle }}</h2>
-                        </div>
-                        <div v-show="hasSecondSubtitle" class="cw-block-headline-second-subtitle">
-                            <h2 :style="textStyle">{{ currentSecondSubtitle }}</h2>
-                        </div>
-                    </div>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <courseware-tabs>
-                    <courseware-tab
-                        :index="0"
-                        :name="$gettext('Layout')"
-                        :selected="true"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Typ') }}
-                                <select v-model="currentStyle">
-                                    <option value="heavy">{{ $gettext('Große Schrift') }}</option>
-                                    <option value="ribbon">{{ $gettext('Band') }}</option>
-                                    <option value="vertical">{{ $gettext('Vertikal') }}</option>
-                                    <option value="bigicon_top">{{ $gettext('Großes Icon oben') }}</option>
-                                    <option value="bigicon_before">{{ $gettext('Großes Icon davor') }}</option>
-                                    <option value="icon_top_lines">{{ $gettext('Icon oben mit Linien') }}</option>
-                                    <option value="skew_text">{{ $gettext('Schief') }}</option>
-                                </select>
-                            </label>
-                            <label>
-                                {{ $gettext('Höhe') }}
-                                <select v-model="currentHeight">
-                                    <option value="full">{{ $gettext('Voll') }}</option>
-                                    <option value="half">{{ $gettext('Halb') }}</option>
-                                    <option value="quarter">{{ $gettext('Viertel') }}</option>
-                                </select>
-                            </label>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="1"
-                        :name="$gettext('Inhalt')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Haupttitel') }}
-                                <input type="text" v-model="currentTitle" />
-                            </label>
-                            <label v-if="hasSubtitle">
-                                {{ $gettext('Untertitel') }}
-                                <input type="text" v-model="currentSubtitle" />
-                            </label>
-                            <label v-if="hasSecondSubtitle">
-                                {{ $gettext('2. Untertitel') }}
-                                <input type="text" v-model="currentSecondSubtitle" />
-                            </label>
-                            <label>
-                                {{ $gettext('Textfarbe') }}
-                                <studip-select
-                                    :options="colors"
-                                    label="hex"
-                                    :reduce="color => color.hex"
-                                    :clearable="false"
-                                    v-model="currentTextColor"
-                                >
-                                    <template #open-indicator="selectAttributes">
-                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
-                                    </template>
-                                    <template #no-options>
-                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                                    </template>
-                                    <template #selected-option="{name, hex}">
-                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                                    </template>
-                                    <template #option="{name, hex}">
-                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                                    </template>
-                                </studip-select>
-                            </label>
-                            <label v-if="hasTextBackgroundColor">
-                                {{ $gettext('Texthintergrundfarbe') }}
-                                <studip-select
-                                    :options="colors"
-                                    label="hex"
-                                    :reduce="color => color.hex"
-                                    :clearable="false"
-                                    v-model="currentTextBackgroundColor"
-                                >
-                                    <template #open-indicator="selectAttributes">
-                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
-                                    </template>
-                                    <template #no-options>
-                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                                    </template>
-                                    <template #selected-option="{name, hex}">
-                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                                    </template>
-                                    <template #option="{name, hex}">
-                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                                    </template>
-                                </studip-select>
-                            </label>
-                            <template v-if="hasIcon">
-                                <label>
-                                    {{ $gettext('Icon') }}
-                                    <studip-select :clearable="false" :options="icons" v-model="currentIcon">
-                                        <template #open-indicator="selectAttributes">
-                                            <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
-                                        </template>
-                                        <template #no-options>
-                                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                                        </template>
-                                        <template #selected-option="option">
-                                            <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
-                                        </template>
-                                        <template #option="option">
-                                            <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
-                                        </template>
-                                    </studip-select>
-                                </label>
-                                <label>
-                                    {{ $gettext('Icon-Farbe') }}
-                                    <studip-select
-                                        :options="iconColors"
-                                        label="name"
-                                        :reduce="iconColor => iconColor.class"
-                                        :clearable="false"
-                                        v-model="currentIconColor"
-                                    >
-                                        <template #open-indicator="selectAttributes">
-                                            <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
-                                        </template>
-                                        <template #no-options>
-                                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                                        </template>
-                                        <template #selected-option="{name, hex}">
-                                            <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                                        </template>
-                                        <template #option="{name, hex}">
-                                            <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                                        </template>
-                                    </studip-select>
-                                </label>
-                            </template>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="2"
-                        :name="$gettext('Hintergrund')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Hintergrundtyp') }}
-                                <select v-model="currentBackgroundType">
-                                    <option value="color">{{ $gettext('Farbe') }}</option>
-                                    <option value="gradient">{{ $gettext('Farbverlauf') }}</option>
-                                    <option value="image">{{ $gettext('Bild') }}</option>
-                                    <option value="structural-element-image">{{ $gettext('Seiten-Bild') }}</option>
-                                </select>
-                            </label>
-                            <label  v-if="currentBackgroundType === 'color'">
-                                {{ $gettext('Hintergrundfarbe') }}
-                                <studip-select
-                                    :options="colors"
-                                    label="hex"
-                                    :reduce="color => color.hex"
-                                    v-model="currentBackgroundColor"
-                                    :clearable="false"
-                                >
-                                    <template #open-indicator="selectAttributes">
-                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
-                                    </template>
-                                    <template #no-options>
-                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                                    </template>
-                                    <template #selected-option="{name, hex}">
-                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                                    </template>
-                                    <template #option="{name, hex}">
-                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                                    </template>
-                                </studip-select>
-                            </label>
-                            <label v-if="currentBackgroundType === 'image'">
-
-                                <div>{{ $gettext('Hintergrundbild') }}</div>
-
-                                <template v-if="currentBackgroundImageId">
-                                    <template v-if="currentBackgroundImageType === 'file-refs'">
-                                        <StockImageThumbnail :url="currentBackgroundURL" width="8rem" contain />
-                                    </template>
-                                    <template v-if="currentBackgroundImageType === 'stock-images'">
-                                        <StockImageSelectableImageCard
-                                            :stock-image="selectedStockImage"
-                                            v-if="selectedStockImage"
-                                            />
-                                    </template>
-                                    <label>
-                                        <button class="button" type="button" @click="onClickRemoveBackgroundImage">
-                                            {{ $gettext('Bild entfernen') }}
-                                        </button>
-                                    </label>
-                                </template>
-
-                                <template v-if="!currentBackgroundImageId">
-                                    <courseware-file-chooser
-                                        v-model="currentBackgroundImageId"
-                                        :isImage="true"
-                                        @selectFile="onSelectFile"
-                                        />
-                                    <div style="margin-block-start: 1em">{{ $gettext('oder') }}</div>
-                                    <button class="button" type="button" @click="showStockImageSelector = true">
-                                        {{ $gettext('Aus dem Bilderpool auswählen') }}
-                                    </button>
-                                    <StockImageSelector
-                                        v-if="showStockImageSelector"
-                                        @close="showStockImageSelector = false"
-                                        @select="onSelectStockImage"
-                                        />
-                                </template>
-
-                            </label>
-                            <label v-if="currentBackgroundType === 'gradient'">
-                                {{ $gettext('Hintergrundfarbverlauf') }}
-                                <select v-model="currentGradient">
-                                    <option value="color-blue">{{ $gettext('Blau') }}</option>
-                                    <option value="color-purple">{{ $gettext('Purpur') }}</option>
-                                    <option value="color-violet">{{ $gettext('Violet') }}</option>
-                                    <option value="color-red">{{ $gettext('Rot') }}</option>
-                                    <option value="color-orange">{{ $gettext('Orange') }}</option>
-                                    <option value="color-yellow">{{ $gettext('Gelb') }}</option>
-                                    <option value="color-green">{{ $gettext('Grün') }}</option>
-                                    <option value="color-petrol">{{ $gettext('Petrol') }}</option>
-                                    <option value="color-grey">{{ $gettext('Grau') }}</option>
-                                    <option value="paper">{{ $gettext('Papier') }}</option>
-                                    <option value="blueprint">{{ $gettext('Blueprint') }}</option>
-                                    <option value="grid">{{ $gettext('Gitter') }}</option>
-                                    <option value="blue-bar">{{ $gettext('Blaue Balken') }}</option>
-                                    <option value="green-bar">{{ $gettext('Grüne Balken') }}</option>
-                                    <option value="faded-sun">{{ $gettext('Faded Sun') }}</option>
-                                    <option value="romantic-sun">{{ $gettext('Romantic Sun') }}</option>
-                                    <option value="bright-rain">{{ $gettext('Bright Rain') }}</option>
-                                    <option value="soft-weather">{{ $gettext('Soft Weather') }}</option>
-                                </select>
-                            </label>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>
-
-            </template>
-            <template #info>{{ $gettext('Informationen zum Blickfang-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFileChooser from './CoursewareFileChooser.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import { blockMixin } from './block-mixin.js';
-import colorMixin from '@/vue/mixins/courseware/colors.js';
-import { mapGetters, mapActions } from 'vuex';
-import contentIcons from './content-icons.js';
-import StockImageSelector from '../stock-images/SelectorDialog.vue';
-import StockImageSelectableImageCard from '../stock-images/SelectableImageCard.vue';
-import StockImageThumbnail from '../stock-images/Thumbnail.vue';
-
-export default {
-    name: 'courseware-headline-block',
-    mixins: [blockMixin, colorMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareFileChooser,
-        CoursewareTabs,
-        CoursewareTab,
-        StockImageSelector,
-        StockImageSelectableImageCard,
-        StockImageThumbnail,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentSubtitle: '',
-            currentSecondSubtitle: '',
-            currentStyle: '',
-            currentHeight: '',
-            currentBackgroundColor: '',
-            currentTextColor: '',
-            currentTextBackgroundColor: '',
-            currentIcon: '',
-            currentIconColor: '',
-            currentBackgroundType: '',
-            currentBackgroundImageId: '',
-            currentBackgroundImageType: '',
-            currentBackgroundImage: {},
-            currentBackgroundURL: '',
-            currentGradient: '',
-            showStockImageSelector: false,
-            selectedStockImage: null,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            fileRefById: 'file-refs/byId',
-            stockImageById: 'stock-images/byId',
-            urlHelper: 'urlHelper',
-            relatedTermOfUse: 'terms-of-use/related',
-            currentStructuralElementImageURL: 'currentStructuralElementImageURL'
-        }),
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        subtitle() {
-            return this.block?.attributes?.payload?.subtitle;
-        },
-        secondSubtitle() {
-            return this.block?.attributes?.payload?.second_subtitle;
-        },
-        style() {
-            return this.block?.attributes?.payload?.style;
-        },
-        height() {
-            return this.block?.attributes?.payload?.height;
-        },
-        backgroundColor() {
-            return this.block?.attributes?.payload?.background_color;
-        },
-        gradient() {
-            return this.block?.attributes?.payload?.gradient;
-        },
-        textColor() {
-            return this.block?.attributes?.payload?.text_color;
-        },
-        textBackgroundColor() {
-            return this.block?.attributes?.payload?.text_background_color ?? '#000000';
-        },
-        icon() {
-            return this.block?.attributes?.payload?.icon;
-        },
-        iconColor() {
-            return this.block?.attributes?.payload?.icon_color;
-        },
-        backgroundImageId() {
-            return this.block?.attributes?.payload?.background_image_id;
-        },
-        backgroundImageType() {
-            return this.block?.attributes?.payload?.background_image_type;
-        },
-        backgroundImage() {
-            return this.block?.attributes?.payload?.background_image;
-        },
-        backgroundType() {
-            return this.block?.attributes?.payload?.background_type;
-        },
-        complementBackgroundColor() {
-            return this.calcComplement(this.backgroundColor);
-        },
-        icons() {
-            return contentIcons;
-        },
-        colors() {
-            return this.mixinColors;
-        },
-        iconColors() {
-            return this.mixinColors.filter(color => color.icon && color.class !== 'studip-lightblue');
-        },
-        textStyle() {
-            let style = {};
-            style.color = this.currentTextColor;
-
-            return style;
-        },
-        headlineStyle() {
-            let style = {};
-            style['background-color'] = this.currentBackgroundColor;
-            if (this.currentBackgroundType === 'image') {
-                style['background-image'] = 'url(' + this.currentBackgroundURL + ')';
-                style['background-color'] = 'transparent';
-            }
-            if (this.currentBackgroundType === 'structural-element-image') {
-                if (this.currentStructuralElementImageURL) {
-                    style['background-image'] = 'url(' + this.currentStructuralElementImageURL + ')';
-                    style['background-color'] = 'transparent';
-                } else {
-                    style['background-color'] = '#28497c';
-                }
-
-            }
-            if (this.hasGradient) {
-                style['background-color'] = 'transparent';
-            }
-
-            return style;
-        },
-        headlineTextboxStyle() {
-            return {
-                rgba: { 'background-color': this.hexToRgbA(this.currentTextBackgroundColor, '0.5') },
-                hex: { 'background-color': this.currentTextBackgroundColor }
-            };
-        },
-        hasSubtitle() {
-            return !['bigicon_before'].includes(this.currentStyle);
-        },
-        subtitleIsSet() {
-            return this.currentSubtitle !== '';
-        },
-        hasSecondSubtitle() {
-            return ['skew_text'].includes(this.currentStyle);
-        },
-        hasIcon() {
-            return ['bigicon_top', 'bigicon_before', 'icon_top_lines'].includes(this.currentStyle);
-        },
-        hasTextBackgroundColor() {
-            return ['ribbon', 'vertical'].includes(this.currentStyle);
-        },
-        hasGradient() {
-            return this.currentBackgroundType === 'gradient';
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            loadFileRef: 'file-refs/loadById',
-            loadStockImage: 'stock-images/loadById',
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentSubtitle = this.subtitle;
-            this.currentSecondSubtitle = this.secondSubtitle;
-            this.currentStyle = this.style;
-            this.currentHeight = this.height;
-            this.currentBackgroundColor = this.backgroundColor;
-            this.currentGradient = this.gradient !== '' ? this.gradient : 'color-blue';
-            this.currentTextColor = this.textColor;
-            this.currentTextBackgroundColor = this.textBackgroundColor;
-            this.currentIcon = this.icon;
-            this.currentIconColor = this.iconColor;
-            this.currentBackgroundType = this.backgroundType;
-            this.currentBackgroundImageId = this.backgroundImageId;
-            this.currentBackgroundImageType = this.backgroundImageType ?? 'file-refs';
-            if (this.currentBackgroundImageId !== '') {
-                this.loadFile();
-            }
-        },
-        async loadFile() {
-            const id = this.currentBackgroundImageId;
-            const type = this.currentBackgroundImageType;
-
-            if (type === 'file-refs') {
-                const options = { include: 'terms-of-use' };
-                await this.loadFileRef({ id: id, options });
-                const fileRef = this.fileRefById({ id: id });
-                if (
-                    fileRef &&
-                    this.relatedTermOfUse({ parent: fileRef, relationship: 'terms-of-use' }).attributes[
-                        'download-condition'
-                    ] === 0
-                ) {
-                    this.updateCurrentBackgroundImage({
-                        id: fileRef.id,
-                        type: 'file-refs',
-                        name: fileRef.attributes.name,
-                        download_url: this.urlHelper.getURL(
-                            'sendfile.php',
-                            { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
-                            true
-                        ),
-                    });
-                }
-            } else if (type === 'stock-images') {
-                await this.loadStockImage({ id });
-                const stockImage = this.stockImageById({ id });
-                if (stockImage) {
-                    this.selectedStockImage = stockImage;
-                    this.updateCurrentBackgroundImage({
-                        id,
-                        type: 'stock-images',
-                        name: stockImage.attributes.title,
-                        download_url:
-                            stockImage.attributes['download-urls']['medium'] ??
-                            stockImage.attributes['download-urls']['small'],
-                    });
-                }
-            }
-        },
-        updateCurrentBackgroundImage(file) {
-            this.currentBackgroundImage = file;
-            this.currentBackgroundImageId = file.id;
-            this.currentBackgroundImageType = file.type;
-            this.currentBackgroundURL = file.download_url;
-        },
-        onSelectFile(file) {
-            this.updateCurrentBackgroundImage({ ...file, type: 'file-refs' });
-        },
-        storeText() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.title = this.currentTitle;
-            attributes.payload.subtitle = this.currentSubtitle;
-            attributes.payload.second_subtitle = this.currentSecondSubtitle;
-            attributes.payload.style = this.currentStyle;
-            attributes.payload.height = this.currentHeight;
-            attributes.payload.text_color = this.currentTextColor;
-            attributes.payload.text_background_color = this.currentTextBackgroundColor;
-            attributes.payload.icon = this.currentIcon;
-            attributes.payload.icon_color = this.currentIconColor;
-            attributes.payload.background_type = this.currentBackgroundType;
-            attributes.payload.background_color = '';
-            attributes.payload.gradient = '';
-            attributes.payload.background_image_id = '';
-            attributes.payload.background_image_type = '';
-
-            if (this.currentBackgroundType === 'color') {
-                attributes.payload.background_color = this.currentBackgroundColor;
-            }
-            if (this.currentBackgroundType === 'image') {
-                attributes.payload.background_image_id = this.currentBackgroundImageId;
-                attributes.payload.background_image_type = this.currentBackgroundImageType;            }
-            if (this.currentBackgroundType === 'gradient') {
-                attributes.payload.gradient = this.currentGradient;
-            }
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-        calcComplement(color) {
-            const RGB = this.calcRGB(color);
-
-            return '#' + this.compToHex(255 - RGB.r) + this.compToHex(255 - RGB.g) + this.compToHex(255 - RGB.b);
-        },
-        calcIconColor(color) {
-            const RGB = this.calcRGB(color);
-
-            return (RGB.r + RGB.g + RGB.b) / 3 > 129 ? 'black' : 'white';
-        },
-        calcRGB(color) {
-            color = color.slice(1); // remove #
-            let val = parseInt(color, 16);
-            let r = val >> 16;
-            let g = (val >> 8) & 0x00ff;
-            let b = val & 0x0000ff;
-
-            if (g > 255) {
-                g = 255;
-            } else if (g < 0) {
-                g = 0;
-            }
-            if (b > 255) {
-                b = 255;
-            } else if (b < 0) {
-                b = 0;
-            }
-
-            return { r: r, g: g, b: b };
-        },
-        compToHex(comp) {
-            let hex = comp.toString(16);
-            return hex.length === 1 ? '0' + hex : hex;
-        },
-        hexToRgbA(hex, a){
-            const RGB = this.calcRGB(hex);
-
-            return 'rgba(' + RGB.r + ',' + RGB.g + ',' + RGB.b + ',' + a +')';
-        },
-        onSelectStockImage(stockImage) {
-            this.updateCurrentBackgroundImage({
-                id: stockImage.id,
-                type: 'stock-images',
-                name: stockImage.attributes.title,
-                download_url:
-                    stockImage.attributes['download-urls']['medium'] ?? stockImage.attributes['download-urls']['small'],
-            });
-            this.selectedStockImage = stockImage;
-            this.showStockImageSelector = false;
-        },
-        onClickRemoveBackgroundImage() {
-            this.currentBackgroundImageId = '';
-            this.currentBackgroundImageType = '';
-            this.selectedStockImage = null;
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareIframeBlock.vue b/resources/vue/components/courseware/CoursewareIframeBlock.vue
deleted file mode 100644
index e1aa21e..0000000
--- a/resources/vue/components/courseware/CoursewareIframeBlock.vue
+++ /dev/null
@@ -1,266 +0,0 @@
-<template>
-    <div class="cw-block cw-block-iframe">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
-                <iframe
-                    v-show="currentUrl.includes('http')"
-                    :src="activeUrl"
-                    :height="currentHeight"
-                    width="100%"
-                    allowfullscreen
-                    sandbox="allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts"
-                />
-                <div v-if="currentCcInfo" class="cw-block-iframe-cc-data">
-                    <span class="cw-block-iframe-cc" :class="['cw-block-iframe-cc-' + currentCcInfo]"></span>
-                    <div class="cw-block-iframe-cc-infos">
-                        <p v-if="currentCcWork">{{ $gettext('Werk') }}: {{ currentCcWork }}</p>
-                        <p v-if="currentCcAuthor">{{ $gettext('Autor') }}: {{ currentCcAuthor }}</p>
-                        <p v-if="currentCcBase">{{ $gettext('Lizenz der Plattform') }}: {{ currentCcBase }}</p>
-                    </div>
-                </div>
-                <div v-show="!currentUrl.includes('http')" :style="{ height: currentHeight + 'px' }"></div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <courseware-tabs>
-                    <courseware-tab
-                        :index="0"
-                        :name="$gettext('Grunddaten')"
-                        :selected="true"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Titel') }}
-                                <input type="text" v-model="currentTitle" />
-                            </label>
-                            <label>
-                                {{ $gettext('URL') }}
-                                <input type="text" v-model="currentUrl" @change="setProtocol" />
-                            </label>
-                            <label>
-                                {{ $gettext('Höhe') }}
-                                <input type="number" v-model="currentHeight" min="0" />
-                            </label>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="1"
-                        :name="$gettext('Nutzerspezifische ID')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Nutzerspezifische ID übergeben') }}
-                                <select v-model="currentSubmitUserId">
-                                    <option value="false">{{ $gettext('Nein') }}</option>
-                                    <option value="true">{{ $gettext('Ja') }}</option>
-                                </select>
-                            </label>
-
-                            <label v-if="currentSubmitUserId === 'true'">
-                                {{ $gettext('Name des Übergabeparameters') }}
-                                <input type="text" v-model="currentSubmitParam" />
-                            </label>
-                            <label v-if="currentSubmitUserId === 'true'">
-                                {{ $gettext('Zufallszeichen für Verschlüsselung (Salt)') }}
-                                <input type="text" v-model="currentSalt" />
-                            </label>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="2"
-                        :name="$gettext('Creative Commons')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Creative Commons Lizenz') }}
-                                <select v-model="currentCcInfo">
-                                    <option value="false">{{ $gettext('Keine') }}</option>
-                                    <option value="by">(by) {{ $gettext('Namensnennung') }}</option>
-                                    <option value="by-sa">
-                                        (by-sa) {{ $gettext('Namensnennung & Weitergabe unter gleichen Bedingungen') }}
-                                    </option>
-                                    <option value="by-nc">
-                                        (by-nc) {{ $gettext('Namensnennung & Nicht kommerziell') }}
-                                    </option>
-                                    <option value="by-nd">
-                                        (by-nd) {{ $gettext('Namensnennung & Keine Bearbeitung') }}
-                                    </option>
-                                    <option value="by-nc-nd">
-                                        (by-nc-nd) {{ $gettext('Namensnennung & Nicht kommerziell & Keine Bearbeitung') }}
-                                    </option>
-                                    <option value="by-nc-sa">
-                                        (by-nc-sa)
-                                        {{ $gettext('Namensnennung & Nicht kommerziell & Weitergabe unter gleichen Bedingungen') }}
-                                    </option>
-                                </select>
-                            </label>
-                            <label v-if="currentCcInfo !== 'false'">
-                                CC {{ $gettext('Werk') }}
-                                <input type="text" v-model="currentCcWork" />
-                            </label>
-                            <label v-if="currentCcInfo !== 'false'">
-                                CC {{ $gettext('Author') }}
-                                <input type="text" v-model="currentCcAuthor" />
-                            </label>
-                            <label v-if="currentCcInfo !== 'false'">
-                                CC {{ $gettext('Lizenz der Plattform') }}
-                                <input type="text" v-model="currentCcBase" />
-                            </label>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum IFrame-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-import md5 from 'md5';
-
-export default {
-    name: 'courseware-iframe-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareTabs,
-        CoursewareTab,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentUrl: '',
-            currentHeight: '',
-            currentSubmitUserId: '',
-            currentSubmitParam: '',
-            currentSalt: '',
-            currentCcInfo: '',
-            currentCcWork: '',
-            currentCcAuthor: '',
-            currentCcBase: '',
-        };
-    },
-    computed: {
-        ...mapGetters(['userId']),
-        url() {
-            return this.block?.attributes?.payload?.url;
-        },
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        height() {
-            return this.block?.attributes?.payload?.height;
-        },
-        submitUserId() {
-            return this.block?.attributes?.payload?.submit_user_id;
-        },
-        submitParam() {
-            return this.block?.attributes?.payload?.submit_param;
-        },
-        salt() {
-            return this.block?.attributes?.payload?.salt;
-        },
-        ccInfo() {
-            return this.block?.attributes?.payload?.cc_info;
-        },
-        ccWork() {
-            return this.block?.attributes?.payload?.cc_work;
-        },
-        ccAuthor() {
-            return this.block?.attributes?.payload?.cc_author;
-        },
-        ccBase() {
-            return this.block?.attributes?.payload?.cc_base;
-        },
-        activeUrl() {
-            if (this.currentUrl) {
-                let url = new URL(this.currentUrl);
-                if (this.currentSubmitUserId === 'true') {
-                    url.searchParams.append(this.currentSubmitParam, md5(this.userId + this.currentSalt));
-                }
-
-                return url.href;
-            }
-
-            return '';
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentUrl = this.url;
-            this.currentHeight = this.height;
-            this.currentSubmitUserId = this.submitUserId;
-            this.currentSubmitParam = this.submitParam;
-            this.currentSalt = this.salt;
-            this.currentCcInfo = this.ccInfo;
-            this.currentCcWork = this.ccWork;
-            this.currentCcAuthor = this.ccAuthor;
-            this.currentCcBase = this.ccBase;
-            this.setProtocol();
-        },
-        setProtocol() {
-            if (location.protocol === 'https:') {
-                if (!this.currentUrl.includes('https:')) {
-                    if (this.currentUrl.includes('http:')) {
-                        this.currentUrl = this.currentUrl.replace('http', 'https');
-                    } else {
-                        this.currentUrl = 'https://' + this.currentUrl;
-                    }
-                }
-            } else if (location.protocol === 'http:') {
-                if (!this.currentUrl.includes('http:') && !this.currentUrl.includes('https:')) {
-                    this.currentUrl = 'http://' + this.currentUrl;
-                }
-            }
-        },
-
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.title = this.currentTitle;
-            attributes.payload.url = this.currentUrl;
-            attributes.payload.height = this.currentHeight;
-            attributes.payload.submit_user_id = this.currentSubmitUserId;
-            attributes.payload.submit_param = this.currentSubmitParam;
-            attributes.payload.salt = this.currentSalt;
-            attributes.payload.cc_info = this.currentCcInfo;
-            attributes.payload.cc_work = this.currentCcWork;
-            attributes.payload.cc_author = this.currentCcAuthor;
-            attributes.payload.cc_base = this.currentCcBase;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/CoursewareImageMapBlock.vue
deleted file mode 100644
index cfcd8f7..0000000
--- a/resources/vue/components/courseware/CoursewareImageMapBlock.vue
+++ /dev/null
@@ -1,787 +0,0 @@
-<template>
-    <div class="cw-block cw-block-image-map" @mousedown="selectShape">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <img :src="currentUrl" class="cw-image-map-original-img" ref="original_img" @load="buildCanvas" />
-                <canvas class="cw-image-map-canvas" ref="canvas"></canvas>
-                <img
-                    class="cw-image-from-canvas"
-                    :src="image_from_canvas"
-                    ref="image_from_canvas"
-                    :usemap="'#' + map_name"
-                />
-                <map ref="map" :name="map_name">
-                    <area
-                        v-for="area in areas"
-                        :key="area.id"
-                        :id="area.id"
-                        :shape="area.shape"
-                        :coords="area.coords"
-                        :title="area.title"
-                        :href="area.external_target"
-                        :target="area.link_target"
-                        @click=" 
-                            if (area.target_type === 'internal') {
-                                areaLink(area.internal_target);
-                            }
-                        "
-                    />
-                </map>
-                <div v-if="showEditMode && viewMode === 'edit' && currentShapes.length > 0"
-                    ref="draggableShapeWrapper" class="cw-draggable-shapes-wrapper">
-                    <vue-resizeable
-                            v-for="(shape, index) in currentShapes"
-                            :key="index"
-                            :index="index"
-                            style="position: absolute"
-                            ref="resizableAreaComponents"
-                            :fitParent="true"
-                            :dragSelector="dragSelector"
-                            :active="handlers"
-                            :left="getShapeOffsetLeft(shape)"
-                            :top="getShapeOffsetTop(shape)"
-                            :width="getShapeWidth(shape)"
-                            :height="getShapeHeight(shape)"
-                            @resize:start="dragStartHandler"
-                            @resize:end="endDraggingShape"
-                            @drag:start="dragStartHandler"
-                            @drag:end="endDraggingShape">
-                        <div class="cw-draggable-area"
-                            :style="{
-                                backgroundColor: getColorRGBA(shape.data.color),
-                                color: shape.data.textcolor ? getColorRGBA(shape.data.textcolor) : '',
-                                borderRadius: getShapeBorderRadius(shape),
-                                border: getShapeBorder(shape),
-                                cursor: selectedShapeIndex !== false ? 'grabbing' : '',
-                            }"
-                            @click="followLink(index)">
-                            {{ shape.data.text }}
-                        </div>
-                    </vue-resizeable>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Bilddatei') }}
-                        <courseware-file-chooser
-                            v-model="currentFileId"
-                            :isImage="true"
-                            @selectFile="updateCurrentFile"
-                        />
-                    </label>
-                    <label>
-                        <a class="button add" @click="addShape('arc')">{{ $gettext('Kreis hinzufügen') }}</a>
-                        <a class="button add" @click="addShape('ellipse')">{{ $gettext('Oval hinzufügen') }}</a>
-                        <a class="button add" @click="addShape('rect')">{{ $gettext('Rechteck hinzufügen') }}</a>
-                    </label>
-                    <courseware-tabs v-if="currentShapes.length > 0">
-                        <courseware-tab
-                            v-for="(shape, index) in currentShapes"
-                            :key="index"
-                            :index="index"
-                            :name="shape.title ? shape.title : ''"
-                            :icon="shape.title === '' ? 'link-extern' : ''"
-                            :selected="index === 0"
-                        >
-                            <label class="col-2">
-                                {{ $gettext('Farbe') }}
-                                <studip-select
-                                    :options="colors"
-                                    label="name"
-                                    :reduce="color => color.class"
-                                    :clearable="false"
-                                    v-model="shape.data.color"
-                                    @input="drawScreen"
-                                >
-                                    <template #open-indicator="selectAttributes">
-                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
-                                    </template>
-                                    <template #no-options>
-                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                                    </template>
-                                    <template #selected-option="{name, rgba}">
-                                        <span class="vs__option-color" :style="{'background-color': rgba}"></span><span>{{name}}</span>
-                                    </template>
-                                    <template #option="{name, rgba}">
-                                        <span class="vs__option-color" :style="{'background-color': rgba}"></span><span>{{name}}</span>
-                                    </template>
-                                </studip-select>
-                            </label>
-                            <label class="col-2">
-                                {{ $gettext('Textfarbe') }}
-                                <studip-select
-                                    :options="colors"
-                                    label="name"
-                                    :reduce="color => color.class"
-                                    :clearable="false"
-                                    v-model="shape.data.textcolor"
-                                    @input="drawScreen"
-                                >
-                                    <template #open-indicator="selectAttributes">
-                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
-                                    </template>
-                                    <template #no-options>
-                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                                    </template>
-                                    <template #selected-option="{name, rgba}">
-                                        <span class="vs__option-color" :style="{'background-color': rgba}"></span><span>{{name}}</span>
-                                    </template>
-                                    <template #option="{name, rgba}">
-                                        <span class="vs__option-color" :style="{'background-color': rgba}"></span><span>{{name}}</span>
-                                    </template>
-                                </studip-select>
-                            </label>
-                            <br>
-                            <template v-if="shape.type === 'arc'">
-                                <label class="col-1">
-                                    X
-                                    <input type="number" v-model="shape.data.centerX" @change="drawScreen" />
-                                </label>
-                                <label class="col-1">
-                                    Y
-                                    <input type="number" v-model="shape.data.centerY" @change="drawScreen" />
-                                </label>
-                                <label class="col-1">
-                                    {{ $gettext('Radius') }}
-                                    <input type="number" v-model="shape.data.radius" @change="drawScreen" />
-                                </label>
-                            </template>
-                            <template v-if="shape.type === 'rect'">
-                                <label class="col-1">
-                                    X
-                                    <input type="number" v-model="shape.data.X" @change="drawScreen" />
-                                </label>
-                                <label class="col-1">
-                                    Y
-                                    <input type="number" v-model="shape.data.Y" @change="drawScreen" />
-                                </label>
-                                <label class="col-1">
-                                    {{ $gettext('Höhe') }}
-                                    <input type="number" v-model="shape.data.height" @change="drawScreen" />
-                                </label>
-                                <label class="col-1">
-                                    {{ $gettext('Breite') }}
-                                    <input type="number" v-model="shape.data.width" @change="drawScreen" />
-                                </label>
-                            </template>
-                            <template v-if="shape.type === 'ellipse'">
-                                <label class="col-1">
-                                    X
-                                    <input type="number" v-model="shape.data.X" @change="drawScreen" />
-                                </label>
-                                <label class="col-1">
-                                    Y
-                                    <input type="number" v-model="shape.data.Y" @change="drawScreen" />
-                                </label>
-                                <label class="col-1">
-                                    {{ $gettext('Radius') }} X
-                                    <input type="number" v-model="shape.data.radiusX" @change="drawScreen" />
-                                </label>
-                                <label class="col-1">
-                                    {{ $gettext('Radius') }} Y
-                                    <input type="number" v-model="shape.data.radiusY" @change="drawScreen" />
-                                </label>
-                            </template>
-                            <label class="col-4">
-                                {{ $gettext('Bezeichnung') }}
-                                <input type="text" v-model="shape.title" />
-                            </label>
-                            <label class="col-4">
-                                {{ $gettext('Beschriftung') }}
-                                <input type="text" v-model="shape.data.text" @change="drawScreen" />
-                            </label>
-                            <br>
-                            <label class="col-2">
-                                {{ $gettext('Art des Links') }}
-                                <select v-model="shape.link_type">
-                                    <option value="internal">{{ $gettext('Interner Link') }}</option>
-                                    <option value="external">{{ $gettext('Externer Link') }}</option>
-                                </select>
-                            </label>
-                            <label v-if="shape.link_type === 'internal'" class="col-2">
-                                {{ $gettext('Ziel des Links') }}
-                                <select v-model="shape.target_internal" @change="drawScreen">
-                                    <option v-for="(el, index) in courseware" :key="index" :value="el.id">
-                                        {{ el.attributes.title }}
-                                    </option>
-                                </select>
-                            </label>
-                            <label v-if="shape.link_type === 'external'" class="col-2">
-                                {{ $gettext('Ziel des Links') }}
-                                <input
-                                    type="text"
-                                    placeholder="https://www.studip.de"
-                                    v-model="shape.target_external"
-                                    @change="
-                                        drawScreen();
-                                        fixUrl(index);
-                                    "
-                                />
-                            </label>
-                            <label>
-                                <a class="button cancel" @click="removeShape(index)"
-                                    >{{ $gettext('Form entfernen') }}</a
-                                >
-                            </label>
-                        </courseware-tab>
-                    </courseware-tabs>
-                </form>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Verweissensitive-Grafik-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFileChooser from './CoursewareFileChooser.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import VueResizeable from 'vrp-vue-resizable';
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-image-map-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareFileChooser,
-        CoursewareTabs,
-        CoursewareTab,
-        VueResizeable,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentFileId: '',
-            currentFile: {},
-            currentShapes: {},
-            context: {},
-            image_from_canvas: '',
-            map_name: '',
-            areas: [],
-            darkColors: ['black', 'darkgrey', 'purple'],
-            colors: [
-                { name: this.$gettext('Transparent'), class: 'transparent', rgba: 'rgba(0,0,0,0)' },
-                { name: this.$gettext('Weiß'), class: 'white', rgba: 'rgba(255,255,255,1)' },
-                { name: this.$gettext('Blau'), class: 'blue', rgba: 'rgba(52,152,219,1)' },
-                { name: this.$gettext('Grün'), class: 'green', rgba: 'rgba(46,204,113,1)' },
-                { name: this.$gettext('Lila'), class: 'purple', rgba: 'rgba(155,89,182,1)' },
-                { name: this.$gettext('Rot'), class: 'red', rgba: 'rgba(231,76,60,1)' },
-                { name: this.$gettext('Gelb'), class: 'yellow', rgba: 'rgba(254,211,48,1)' },
-                { name: this.$gettext('Orange'), class: 'orange', rgba: 'rgba(243,156,18,1)' },
-                { name: this.$gettext('Grau'), class: 'grey', rgba: 'rgba(236, 240, 241,1)' },
-                { name: this.$gettext('Dunkelgrau'), class: 'darkgrey', rgba: 'rgba(52,73,94,1)' },
-                { name: this.$gettext('Schwarz'), class: 'black', rgba: 'rgba(0,0,0,1)' }
-            ],
-            file: null,
-            dragSelector: ".cw-draggable-area",
-            handlers: ["r", "rb", "b", "lb", "l", "lt", "t", "rt"],
-            draggedShapeWidth: 50,
-            draggedShapeHeight: 50,
-            selectedShapeIndex: false,
-            draggingActive: false,
-            showEditMode: false,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            courseware: 'courseware-structural-elements/all',
-            fileRefById: 'file-refs/byId',
-            urlHelper: 'urlHelper',
-            viewMode: 'viewMode',
-        }),
-        fileId() {
-            return this.block?.attributes?.payload?.file_id;
-        },
-        shapes() {
-            return this.block?.attributes?.payload?.shapes;
-        },
-        currentUrl() {
-            if (this.currentFile.download_url !== 'undefined') {
-                return this.currentFile.download_url;
-            } else {
-                return '';
-            }
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            loadFileRef: 'file-refs/loadById',
-        }),
-        async initCurrentData(event) {
-            this.showEditMode = Boolean(event);
-            this.currentFileId = this.fileId;
-            this.currentShapes = JSON.parse(JSON.stringify(this.shapes));
-            await this.loadFile();
-            this.buildCanvas();
-        },
-        async loadFile() {
-            const id = this.currentFileId;
-            await this.loadFileRef({ id });
-            const fileRef = this.fileRefById({ id });
-
-            if (fileRef) {
-                this.updateCurrentFile({
-                    id: fileRef.id,
-                    name: fileRef.attributes.name,
-                    download_url: this.urlHelper.getURL(
-                        'sendfile.php',
-                        { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
-                        true
-                    ),
-                });
-            }
-        },
-        updateCurrentFile(file) {
-            this.currentFile = file;
-            this.currentFileId = file.id;
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.file_id = this.currentFileId;
-            attributes.payload.shapes = this.currentShapes;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-
-        buildCanvas() {
-            let canvas = this.$refs.canvas;
-            let original_img = this.$refs.original_img;
-            canvas.width = 1085;
-            if (original_img.height > 0) {
-                canvas.height = Math.round((canvas.width / original_img.width) * original_img.height);
-            } else {
-                canvas.height = 484;
-            }
-            this.context = canvas.getContext('2d');
-            this.drawScreen();
-        },
-        drawScreen() {
-            let context = this.context;
-            let view = this;
-            let outlineImage = new Image();
-            outlineImage.src = this.currentUrl;
-            outlineImage.onload = function () {
-                context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
-                context.fillStyle = '#ffffff';
-                context.fillRect(0, 0, context.canvas.width, context.canvas.height); // set background
-                if (outlineImage.src !== '') {
-                    context.drawImage(outlineImage, 0, 0, context.canvas.width, context.canvas.height);
-                }
-                view.drawShapes();
-
-                if (!(view.$refs.canvas.length > 0)) {
-                    view.image_from_canvas = view.context.canvas.toDataURL('image/jpeg', 1.0);
-                    view.mapImage();
-                }
-            };
-        },
-        drawShapes() {
-            let context = this.context;
-            let view = this;
-            this.currentShapes.forEach((value, index) => {
-                // skip the selected shape when redrawing so it disappears while dragging the shape
-                if (this.selectedShapeIndex !== index) {
-                    let shape = value;
-                    let text = shape.data.text;
-                    let shape_width = 0;
-                    let shape_height = 0;
-                    let text_X = 0;
-                    let text_Y = 0;
-
-                    context.beginPath();
-                    switch (shape.type) {
-                        case 'arc':
-                            shape_width = Math.round((2 * shape.data.radius) / Math.sqrt(2)) * 0.85;
-                            shape_height = shape_width / 0.85;
-                            text_X = shape.data.centerX;
-                            text_Y = shape.data.centerY - shape.data.radius * 0.75;
-                            context.arc(shape.data.centerX, shape.data.centerY, shape.data.radius, 0, 2 * Math.PI); // x, y, r, startAngle, endAngle ... Angle in radians!
-                            context.fillStyle = view.colors.filter((color) => {return color.class === shape.data.color})[0].rgba;
-                            context.fill();
-                            break;
-                        case 'ellipse':
-                            shape_width = shape.data.radiusX;
-                            shape_height = shape.data.radiusY * 1.75;
-                            text_X = shape.data.X;
-                            text_Y = shape.data.Y - shape.data.radiusY * 0.8;
-                            context.ellipse(
-                                shape.data.X,
-                                shape.data.Y,
-                                shape.data.radiusX,
-                                shape.data.radiusY,
-                                0,
-                                0,
-                                2 * Math.PI
-                            );
-                            context.fillStyle = view.colors.filter((color) => {return color.class === shape.data.color})[0].rgba;
-                            context.fill();
-                            break;
-                        case 'rect':
-                            shape_width = shape.data.width;
-                            shape_height = shape.data.height;
-                            text_X = shape.data.X + shape.data.width / 2;
-                            text_Y = shape.data.Y;
-                            context.rect(shape.data.X, shape.data.Y, shape.data.width, shape.data.height);
-                            context.fillStyle = view.colors.filter((color) => {return color.class === shape.data.color})[0].rgba;
-                            context.fill();
-                            break;
-                        default:
-                            return;
-                    }
-
-                    if (text && shape.data.color !== 'transparent') {
-                        text = view.fitTextToShape(context, text, shape_width);
-                        context.textAlign = 'center';
-                        context.font = '14px Arial';
-                        if (shape.data.textcolor) {
-                            context.fillStyle = this.getColorRGBA(shape.data.textcolor);
-                        } else {
-                            if (view.darkColors.indexOf(shape.data.color) > -1) {
-                                context.fillStyle = '#ffffff';
-                            } else {
-                                context.fillStyle = '#000000';
-                            }
-                        }
-                        let lineHeight = shape_height / (text.length + 1);
-                        text.forEach((value, key) => {
-                            context.fillText(value, text_X, text_Y + lineHeight * (key + 1));
-                        });
-                    }
-
-                    context.closePath();
-                }
-            });
-        },
-        fitTextToShape( context , text, shapeWidth) {
-            shapeWidth = shapeWidth || 0;
-
-            let newText = [];
-
-            if (shapeWidth <= 0) {
-                return [text];
-            }
-            let words = text.split(' ');
-            let i = 1;
-            while (words.length > 0 && i <= words.length) {
-                let word = words.slice(0, i).join(' ');
-                let wordWidth = context.measureText(word).width + 2;
-                if ( wordWidth > shapeWidth ) {
-                    if (i === 1) {
-                        i = 2;
-                    }
-                    newText.push(words.slice(0, i - 1).join(' '));
-                    words = words.splice(i - 1);
-                    i = 1;
-                }
-                else {
-                    i++;
-                }
-            }
-            if (i > 0) {
-                newText.push(words.join(' '));
-            }
-
-            return newText;
-
-        },
-        mapImage() {
-            let view = this;
-            // generate map name
-            let map_name = 'cw-image-map-' + Math.round(Math.random() * 100);
-            this.map_name = map_name;
-
-            // insert areas
-            this.areas = [];
-            this.currentShapes.forEach((value, key) => {
-                let shape = value;
-                let area = {};
-                let coords = '';
-                let x = 0;
-                let y = 0;
-                area.id = 'shape-' + key;
-
-                switch (shape.type) {
-                    case 'arc':
-                        area.shape = 'circle';
-                        area.coords = shape.data.centerX + ', ' + shape.data.centerY + ', ' + shape.data.radius;
-                        break;
-                    case 'ellipse':
-                        for (let theta = 0; theta < 2 * Math.PI; theta += (2 * Math.PI) / 20) {
-                            x = parseInt(shape.data.X) + Math.round(parseInt(shape.data.radiusX) * Math.cos(theta));
-                            y = parseInt(shape.data.Y) + Math.round(parseInt(shape.data.radiusY) * Math.sin(theta));
-                            coords = coords + x + ',' + y + ',';
-                        }
-                        area.shape = 'poly';
-                        area.coords = coords;
-                        break;
-                    case 'rect':
-                    case 'text':
-                        x = parseInt(shape.data.X) + parseInt(shape.data.width);
-                        y = parseInt(shape.data.Y) + parseInt(shape.data.height);
-                        area.shape = 'rect';
-                        area.coords = shape.data.X + ', ' + shape.data.Y + ', ' + x + ', ' + y;
-                        break;
-                }
-                area.title = shape.title;
-                shape.link_type === 'external'
-                    ? (area.external_target = shape.target_external)
-                    : (area.external_target = '#');
-                if (shape.link_type === 'internal') {
-                    area.internal_target = shape.target_internal;
-                } else {
-                    area.internal_target = '';
-                }
-                shape.link_type === 'external' ? (area.link_target = '_blank') : (area.link_target = '_self');
-                area.link_type = shape.link_type;
-                area.target_type = shape.link_type;
-                view.areas.push(area);
-            });
-        },
-        areaLink(target) {
-            this.$router.push(target);
-        },
-
-        //edit methods
-        addShape(addtype) {
-            let data = {};
-            switch (addtype) {
-                case 'arc':
-                    data = {
-                        centerX: 50,
-                        centerY: 50,
-                        radius: 50,
-                        color: 'blue',
-                        border: false,
-                        text: '',
-                    };
-                    break;
-                case 'rect':
-                    data = {
-                        X: 50,
-                        Y: 50,
-                        height: 100,
-                        width: 50,
-                        color: 'blue',
-                        border: false,
-                        text: '',
-                    };
-                    break;
-                case 'ellipse':
-                    data = {
-                        X: 50,
-                        Y: 50,
-                        radiusX: 50,
-                        radiusY: 20,
-                        color: 'blue',
-                        border: false,
-                        text: '',
-                    };
-                    break;
-            }
-            this.currentShapes.push({
-                type: addtype,
-                data: data,
-                title: '',
-                link_type: 'external',
-                target_internal: '',
-                target_external: '',
-            });
-            this.buildCanvas();
-        },
-        removeShape(index) {
-            this.currentShapes.splice(index, 1);
-            this.drawScreen();
-        },
-        fixUrl(index) {
-            let url = this.currentShapes[index].target_external;
-            if (url !== '' && url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
-                url = 'https://' + url;
-            }
-            this.currentShapes[index].target_external = url;
-        },
-        dragStartHandler(data) {
-            // redraw screen now that a shape was selected so that it disappears while dragging or resizing
-            this.drawScreen();
-        },
-        selectShape(data) {
-            // set current draggable div shape to canvas shape coordinates
-            let canvas = this.$refs.image_from_canvas;
-            let canvasSpecs = canvas.getBoundingClientRect();
-            let mouseX = (data.clientX - canvasSpecs.left) * (canvas.width/canvasSpecs.width);
-            let mouseY = (data.clientY - canvasSpecs.top) * (canvas.height/canvasSpecs.height);
-            this.currentShapes.forEach((value, key) => {
-                let shape = value;
-                // if the event target is the draggable area, check for the shape area normally
-                // else check if the click was on a resizable area that belongs to a shape since 
-                // resizable areas are partly outside the shape
-                if (data.target.classList.contains('cw-draggable-area')) {
-                    if (this.mouseHit(mouseX, mouseY, shape)) {
-                        this.selectedShapeIndex = key;
-                    }
-                } else {
-                    mouseX = data.target.parentElement.offsetLeft;
-                    mouseY = data.target.parentElement.offsetTop;
-                    if (shape.type == 'arc') {
-                        mouseX += shape.data.radius;
-                        mouseY += shape.data.radius;
-                    }
-                    if (shape.type == 'rect') {
-                        mouseX += shape.data.width / 2;
-                        mouseY += shape.data.height / 2;
-                    }
-                    if (shape.type == 'ellipse') {
-                        mouseX += shape.data.radiusX;
-                        mouseY += shape.data.radiusY;
-                    }
-                    if (this.mouseHit(mouseX, mouseY, shape)) {
-                        this.selectedShapeIndex = key;
-                    }
-                }
-            });
-        },
-        endDraggingShape(data) {
-            this.draggingActive = true;
-            // transfer div shape data to canvas according to shape
-            let shape = this.currentShapes[this.selectedShapeIndex];
-            if (shape.type == 'arc') {
-                let circle_width = data.width != shape.data.radius * 2? data.width : data.height;
-                // if the shape was clicked and not dragged, set the dragging status to false to follow the link
-                if (shape.data.centerX == data.left + shape.data.radius || shape.data.centerY == data.top + shape.data.radius) {
-                    this.draggingActive = false;
-                }
-                shape.data.radius = circle_width / 2;
-                shape.data.centerX = data.left + shape.data.radius;
-                shape.data.centerY = data.top + shape.data.radius;
-            }
-            if (shape.type == 'rect') {
-                if (shape.data.X == data.left || shape.data.Y == data.top) {
-                    this.draggingActive = false;
-                }
-                shape.data.X = data.left;
-                shape.data.Y = data.top;
-                shape.data.width = data.width;
-                shape.data.height = data.height;
-            }
-            if (shape.type == 'ellipse') {
-                if (shape.data.X == data.left + shape.data.radiusX || shape.data.Y == data.top + shape.data.radiusY) {
-                    this.draggingActive = false;
-                }
-                shape.data.radiusX = data.width / 2;
-                shape.data.radiusY = data.height / 2;
-                shape.data.X = data.left + shape.data.radiusX;
-                shape.data.Y = data.top + shape.data.radiusY;
-            }
-            // unselect shape to stop skipping the selected shape when drawing the canvas
-            this.selectedShapeIndex = false;
-            this.drawScreen();
-        },
-        mouseHit(mouseX, mouseY, shape) {
-            // check if the mouseclick was on a shape and return true if it was
-            if (shape.type == 'arc') {
-                let dx = shape.data.centerX - mouseX;
-                let dy = shape.data.centerY - mouseY;
-                return (dx*dx + dy*dy < shape.data.radius*shape.data.radius);
-            }
-            if ((shape.type == 'rect') || (shape.type == 'text')) {
-                let dx = mouseX - shape.data.X;
-                let dy = mouseY - shape.data.Y;
-                return ((dx <= shape.data.width) && (dy <= shape.data.height) && (dx >= 0) && (dy >= 0));
-            }
-            if (shape.type == 'ellipse') {
-                let dx = shape.data.X - mouseX;
-                let dy = shape.data.Y - mouseY;
-                return ((Math.abs(dx) < shape.data.radiusX) && (Math.abs(dy) < shape.data.radiusY));
-            }
-        },
-        getColorRGBA(color) {
-            return this.colors.filter((col) => {return col.class === color})[0].rgba;
-        },
-        getShapeBorder(shape) {
-            return shape.data.color === 'transparent' ? 'dashed thin #000' : 'none';
-        },
-        getShapeBorderRadius(shape) {
-            if (shape.type == 'rect') {
-                return 0;
-            } else {
-                return '50%';
-            }
-        },
-        getShapeOffsetLeft(shape) {
-            if (shape.type == 'arc') {
-                return parseInt(shape.data.centerX - shape.data.radius);
-            }
-            if (shape.type == 'rect') {
-                return parseInt(shape.data.X);
-            }
-            if (shape.type == 'ellipse') {
-                return parseInt(shape.data.X) - shape.data.radiusX;
-            }
-        },
-        getShapeOffsetTop(shape) {
-            if (shape.type == 'arc') {
-                return parseInt(shape.data.centerY - shape.data.radius);
-            }
-            if (shape.type == 'rect') {
-                return parseInt(shape.data.Y);
-            }
-            if (shape.type == 'ellipse') {
-                return parseInt(shape.data.Y) - shape.data.radiusY;
-            }
-        },
-        getShapeWidth(shape) {
-            if (shape.type == 'arc') {
-                return parseInt(shape.data.radius * 2);
-            }
-            if (shape.type == 'rect') {
-                return parseInt(shape.data.width);
-            }
-            if (shape.type == 'ellipse') {
-                return parseInt(shape.data.radiusX * 2);
-            }
-        },
-        getShapeHeight(shape) {
-            if (shape.type == 'arc') {
-                return parseInt(shape.data.radius * 2);
-            }
-            if (shape.type == 'rect') {
-                return parseInt(shape.data.height);
-            }
-            if (shape.type == 'ellipse') {
-                return parseInt(shape.data.radiusY * 2);
-            }
-        },
-        followLink(index) {
-            if (!this.draggingActive) {
-                this.$refs.map.areas[index].click();
-            }
-        },
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareImportWidget.vue b/resources/vue/components/courseware/CoursewareImportWidget.vue
deleted file mode 100644
index e57f325..0000000
--- a/resources/vue/components/courseware/CoursewareImportWidget.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-    <sidebar-widget :title="$gettext('Import')">
-        <template #content>
-            <ul class="widget-list widget-links cw-import-widget">
-                <li class="cw-import-widget-archive">
-                    <button @click="importElements">
-                        {{ $gettext('Seiten importieren') }}
-                    </button>
-                </li>
-                <li class="cw-import-widget-copy">
-                    <button @click="copyElements">
-                        {{ $gettext('Seiten kopieren') }}
-                    </button>
-                </li>
-                <li v-if="inCourseContext && userIsTeacher" class="cw-action-widget-link">
-                    <button @click="linkElement">
-                        {{ $gettext('Seiten verknüpfen') }}
-                    </button>
-                </li>
-            </ul>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-import-widget',
-    components: {
-        SidebarWidget,
-    },
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            userIsTeacher: 'userIsTeacher',
-        }),
-        inCourseContext() {
-            return this.context.type === 'courses';
-        }
-    },
-    methods: {
-        ...mapActions({
-            showElementImportDialog: 'showElementImportDialog',
-            showElementCopyDialog: 'showElementCopyDialog',
-            showElementLinkDialog: 'showElementLinkDialog',
-        }),
-        importElements() {
-            this.showElementImportDialog(true);
-        },
-        copyElements() {
-            this.showElementCopyDialog(true);
-        },
-        linkElement() {
-            this.showElementLinkDialog(true);
-        },
-    },
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareKeyPointBlock.vue b/resources/vue/components/courseware/CoursewareKeyPointBlock.vue
deleted file mode 100644
index 687ea0b..0000000
--- a/resources/vue/components/courseware/CoursewareKeyPointBlock.vue
+++ /dev/null
@@ -1,179 +0,0 @@
-<template>
-    <div class="cw-block cw-block-key-point">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div class="cw-keypoint-content" :class="['cw-keypoint-' + currentColor]">
-                    <studip-icon v-if="currentIcon" size="48" :shape="currentIcon" :role="currentRole"/>
-                    <p class="cw-keypoint-sentence">{{currentText}}</p>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label class="col-4">
-                        {{ $gettext('Merksatz') }}
-                        <input
-                            type="text"
-                            name="cw-keypoint-content"
-                            class="cw-keypoint-set-content"
-                            v-model="currentText"
-                            spellcheck="true"
-                        />
-                    </label>
-                    <br>
-                    <label class="col-2">
-                        {{ $gettext('Farbe') }}
-                        <studip-select
-                            :options="colors"
-                            label="icon"
-                            :clearable="false"
-                            :reduce="option => option.icon"
-                            v-model="currentColor"
-                        >
-                            <template #open-indicator="selectAttributes">
-                                <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
-                            </template>
-                            <template #no-options>
-                                {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                            </template>
-                            <template #selected-option="{name, hex}">
-                                <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                            </template>
-                            <template #option="{name, hex}">
-                                <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                            </template>
-                        </studip-select>
-                    </label>
-                    <label class="col-2">
-                        {{ $gettext('Icon') }}
-                        <studip-select :options="icons" :clearable="false" v-model="currentIcon">
-                            <template #open-indicator="selectAttributes">
-                                <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
-                            </template>
-                            <template #no-options>
-                                {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                            </template>
-                            <template #selected-option="option">
-                                <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
-                            </template>
-                            <template #option="option">
-                                <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
-                            </template>
-                        </studip-select>
-                    </label>
-                </form>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Merksatz-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import colorMixin from '@/vue/mixins/courseware/colors.js';
-import { blockMixin } from './block-mixin.js';
-import { mapActions } from 'vuex';
-import contentIcons from './content-icons.js';
-
-export default {
-    name: 'courseware-key-point-block',
-    mixins: [blockMixin, colorMixin],
-    components: {
-        CoursewareDefaultBlock,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentText: '',
-            currentColor: '',
-            currentIcon: '',
-        };
-    },
-    computed: {
-        file() {
-            return `icons/${this.color}/${this.icon}.svg`;
-        },
-        icons() {
-            return contentIcons;
-        },
-        colors() {
-            return this.mixinColors.filter(color => 
-                color.icon && color.class !== 'white' && color.class !== 'studip-lightblue'
-            );
-        },
-        text() {
-            return this.block?.attributes?.payload?.text;
-        },
-        color() {
-            return this.block?.attributes?.payload?.color;
-        },
-        icon() {
-            return this.block?.attributes?.payload?.icon;
-        },
-        currentRole() {
-            switch (this.currentColor) {
-                case 'black':
-                    return 'info';
-
-                case 'grey':
-                    return 'inactive';
-
-                case 'green':
-                    return 'status-green';
-
-                case 'red':
-                    return 'status-red';
-
-                case 'white':
-                    return 'info_alt';
-
-                case 'yellow':
-                    return 'status-yellow';
-
-                case 'blue':
-                default:
-                    return 'clickable';
-            }
-        }
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            this.currentText = this.text;
-            this.currentColor = this.color;
-            this.currentIcon = this.icon;
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.text = this.currentText;
-            attributes.payload.color = this.currentColor;
-            attributes.payload.icon = this.currentIcon;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareLinkBlock.vue b/resources/vue/components/courseware/CoursewareLinkBlock.vue
deleted file mode 100644
index f2c8cf9..0000000
--- a/resources/vue/components/courseware/CoursewareLinkBlock.vue
+++ /dev/null
@@ -1,152 +0,0 @@
-<template>
-    <div class="cw-block cw-block-link">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="currentType === 'external'">
-                    <a :href="currentUrl" target="_blank">
-                        <div class="cw-link external">
-                            <span class="cw-link-title">{{ currentTitle }}</span>
-                        </div>
-                    </a>
-                </div>
-                <div v-if="currentType === 'internal'">
-                    <router-link :to="{ name: 'CoursewareStructuralElement', params: { id: currentTarget } }">
-                        <div class="cw-link internal">
-                            <span class="cw-link-title">
-                                {{ currentTitle }}
-                            </span>
-                        </div>
-                    </router-link>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Titel') }}
-                        <input type="text" v-model="currentTitle" />
-                    </label>
-                    <label>
-                        {{ $gettext('Art des Links') }}
-                        <select v-model="currentType">
-                            <option value="external">{{ $gettext('Extern') }}</option>
-                            <option value="internal">{{ $gettext('Intern') }}</option>
-                        </select>
-                    </label>
-                    <label v-show="currentType === 'external'">
-                        {{ $gettext('URL') }}
-                        <input type="text" v-model="currentUrl" @change="fixUrl" />
-                    </label>
-                    <label v-show="currentType === 'internal'">
-                        {{ $gettext('Seite') }}
-                        <select v-model="currentTarget">
-                            <option v-for="(el, index) in courseware" :key="index" :value="el.id">
-                                {{ el.attributes.title }}
-                            </option>
-                        </select>
-                    </label>
-                </form>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Link-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-link-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentType: '',
-            currentTarget: '',
-            currentUrl: '',
-            currentTitle: '',
-        };
-    },
-    computed: {
-        ...mapGetters({
-            courseware: 'courseware-structural-elements/all',
-        }),
-        type() {
-            return this.block?.attributes?.payload?.type;
-        },
-        target() {
-            return this.block?.attributes?.payload?.target;
-        },
-        url() {
-            return this.block?.attributes?.payload?.url;
-        },
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            companionWarning: 'companionWarning',
-        }),
-        initCurrentData() {
-            this.currentType = this.type;
-            this.currentTarget = this.target;
-            this.currentUrl = this.url;
-            this.currentTitle = this.title;
-            this.fixUrl();
-        },
-        fixUrl() {
-            if (
-                this.currentUrl.indexOf('http://') !== 0 &&
-                this.currentUrl.indexOf('https://') !== 0 &&
-                this.currentUrl !== ''
-            ) {
-                this.currentUrl = 'https://' + this.currentUrl;
-            }
-        },
-        storeBlock() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.type = this.currentType;
-            attributes.payload.target = this.currentTarget;
-            attributes.payload.url = this.currentUrl;
-            attributes.payload.title = this.currentTitle;
-            if (this.currentType === 'internal' && this.currentTarget === '') {
-                this.companionWarning({
-                    info: this.$gettext('Bitte wählen Sie eine Seite als Ziel aus.')
-                });
-                return false;
-            } else {
-                this.updateBlock({
-                    attributes: attributes,
-                    blockId: this.block.id,
-                    containerId: this.block.relationships.container.data.id,
-                });
-            }
-
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareListContainer.vue b/resources/vue/components/courseware/CoursewareListContainer.vue
deleted file mode 100644
index 7ac036a..0000000
--- a/resources/vue/components/courseware/CoursewareListContainer.vue
+++ /dev/null
@@ -1,287 +0,0 @@
-<template>
-    <courseware-default-container
-        :container="container"
-        containerClass="cw-container-list"
-        :canEdit="canEdit"
-        :isTeacher="isTeacher"
-        @storeContainer="storeContainer"
-    >
-        <template v-slot:containerContent>
-            <ul v-if="!showEditMode || currentElementisLink"  class="cw-container-list-block-list">
-                <li v-for="block in blocks" :key="block.id" class="cw-block-item">
-                    <component :is="component(block)" :block="block" :canEdit="canEdit" :isTeacher="isTeacher" />
-                </li>
-            </ul>
-            <template v-else>
-                <template v-if="!processing">
-                    <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
-                    <span id="operation" class="assistive-text">
-                        {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
-                    </span>
-                    <draggable
-                        v-if="showEditMode && canEdit"
-                        class="cw-container-list-block-list cw-container-list-sort-mode"
-                        tag="ol"
-                        role="listbox"
-                        v-model="blockList"
-                        v-bind="dragOptions"
-                        handle=".cw-sortable-handle"
-                        group="blocks"
-                        @start="isDragging = true"
-                        @end="dropBlock"
-                        ref="sortables"
-                        :containerId="container.id"
-                        sectionId="0"
-                    >
-                        <li
-                            v-for="block in blockList"
-                            :key="block.id"
-                            class="cw-block-item cw-block-item-sortable"
-                        >
-                            <span
-                                :class="{ 'cw-sortable-handle-dragging': isDragging }"
-                                class="cw-sortable-handle"
-                                tabindex="0"
-                                role="button"
-                                aria-describedby="operation"
-                                :ref="'sortableHandle' + block.id"
-                                @keydown="keyHandler($event, block.id)"
-                            ></span>
-                            <component
-                                :is="component(block)"
-                                :block="block"
-                                :canEdit="canEdit"
-                                :isTeacher="isTeacher"
-                                :class="{ 'cw-block-item-selected': keyboardSelected === block.id}"
-                                :blockId="block.id"
-                            />
-                        </li>
-                    </draggable>
-                    <courseware-block-adder-area :container="container" :section="0" />
-                </template>
-                <div v-else class="progress-wrapper" :style="{ height: contentHeight + 'px' }">
-                    <studip-progress-indicator :description="$gettext('Vorgang wird bearbeitet...')" />
-                </div>
-            </template>
-        </template>
-    </courseware-default-container>
-</template>
-
-<script>
-import ContainerComponents from './container-components.js';
-import containerMixin from '../../mixins/courseware/container.js';
-import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-import draggable from 'vuedraggable';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-list-container',
-    mixins: [containerMixin],
-    components: Object.assign(ContainerComponents, {
-        draggable,
-        StudipProgressIndicator
-    }),
-    props: {
-        container: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-        canAddElements: Boolean,
-    },
-    data() {
-        return {
-            isDragging: false,
-            dragOptions: {
-                animation: 0,
-                group: this.container.id,
-                disabled: false,
-                ghostClass: "block-ghost"
-            },
-            blockList: [],
-            processing: false,
-            contentHeight: 0,
-            keyboardSelected: null,
-            assistiveLive: ''
-        };
-    },
-    computed: {
-        ...mapGetters({
-            blockById: 'courseware-blocks/byId',
-            containerById: 'courseware-containers/byId',
-            viewMode: 'viewMode',
-            currentElementisLink: 'currentElementisLink'
-        }),
-        blocked() {
-            return this.container?.relationships?.['edit-blocker']?.data !== null;
-        },
-        blockerId() {
-            return this.blocked ? this.container?.relationships?.['edit-blocker']?.data?.id : null;
-        },
-        blockedByThisUser() {
-            return this.blocked && this.userId === this.blockerId;
-        },
-        blockedByAnotherUser() {
-            return this.blocked && this.userId !== this.blockerId;
-        },
-        showEditMode() {
-            return this.viewMode === 'edit';
-        },
-        blocks() {
-            if (!this.container) {
-                return [];
-            }
-            let containerBlocks = this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })).filter(Boolean);
-            let unallocated = new Set(containerBlocks.map(({ id }) => id));
-            let sortedBlocks = this.container.attributes.payload.sections[0].blocks.map((id) => this.blockById({ id })).filter(Boolean);
-            sortedBlocks.forEach(({ id }) => unallocated.delete(id));
-            let unallocatedBlocks = [...unallocated].map((id) => this.blockById({ id }));
-
-            return sortedBlocks.concat(unallocatedBlocks);
-        },
-    },
-    methods: {
-        ...mapActions({
-            updateContainer: 'updateContainer',
-            loadContainer: 'courseware-containers/loadById',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-            companionInfo: 'companionInfo'
-        }),
-        storeContainer(data) {
-        },
-        initCurrentData() {
-            this.blockList = this.blocks;
-        },
-        async storeSort() {
-            this.contentHeight = this.$refs.sortables.$el.offsetHeight;
-            const timeout = setTimeout(() => this.processing = true, 800);
-            if (this.blockedByAnotherUser) {
-                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
-                clearTimeout(timeout);
-                this.processing = false;
-                this.loadContainer({id : this.container.id });
-                return false;
-            }
-            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-            let currentContainer = this.container;
-            currentContainer.attributes.payload.sections[0].blocks = this.blockList.map(block => {return block.id});
-            await this.updateContainer({
-                container: currentContainer,
-                structuralElementId: currentContainer.relationships['structural-element'].data.id,
-            });
-            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
-            await this.loadContainer({id : this.container.id });
-            this.initCurrentData();
-            clearTimeout(timeout);
-            this.processing = false;
-            this.assistiveLive = '';
-        },
-        component(block) {
-            if (block.attributes["block-type"] !== undefined) {
-                return 'courseware-' + block.attributes["block-type"] + '-block';
-            }
-            return null;
-        },
-        keyHandler(e, blockId) {
-            switch (e.keyCode) {
-                case 27: // esc
-                    this.abortKeyboardSorting(blockId);
-                    break;
-                case 13: // enter
-                    e.preventDefault();
-                    if (this.keyboardSelected) {
-                        this.storeKeyboardSorting(blockId);
-                    } else {
-                        this.keyboardSelected = blockId;
-                        const block = this.blockById({id: blockId});
-                        const index = this.blockList.findIndex(b => b.id === block.id);
-                        this.assistiveLive = 
-                            this.$gettextInterpolate(
-                                this.$gettext('%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
-                                , {blockTitle: block.attributes.title, pos: index + 1, listLength: this.blockList.length}
-                            );
-                    }
-                    break;
-            }
-            if (this.keyboardSelected) {
-                switch (e.keyCode) {
-                    case 9: //tab
-                        this.abortKeyboardSorting(blockId);
-                        break;
-                    case 38: // up
-                        e.preventDefault();
-                        this.moveItemUp(blockId);
-                        break;
-                    case 40: // down
-                        e.preventDefault();
-                        this.moveItemDown(blockId);
-                        break;
-                }
-            }
-        },
-        moveItemUp(blockId) {
-            const currentIndex = this.blockList.findIndex(block => block.id === blockId);
-            if (currentIndex !== 0) {
-                const block = this.blockById({id: blockId});
-                const newPos = currentIndex - 1;
-                this.blockList.splice(newPos, 0, this.blockList.splice(currentIndex, 1)[0]);
-                this.assistiveLive = 
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.blockList.length}
-                    );
-            }
-        },
-        moveItemDown(blockId) {
-            const currentIndex = this.blockList.findIndex(block => block.id === blockId);
-            if (this.blockList.length - 1 > currentIndex) {
-                const block = this.blockById({id: blockId});
-                const newPos = currentIndex + 1;
-                this.blockList.splice(newPos, 0, this.blockList.splice(currentIndex, 1)[0]);
-                this.assistiveLive = 
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.blockList.length}
-                    );
-            }
-        },
-        abortKeyboardSorting(blockId) {
-            const block = this.blockById({id: blockId});
-            this.keyboardSelected = null;
-            this.assistiveLive = 
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, Neuordnung abgebrochen.')
-                    , {blockTitle: block.attributes.title}
-                );
-            this.initCurrentData();
-        },
-        storeKeyboardSorting(blockId) {
-            const block = this.blockById({id: blockId});
-            const currentIndex = this.blockList.findIndex(block => block.id === blockId);
-            this.keyboardSelected = null;
-            this.assistiveLive = 
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.')
-                    , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.blockList.length}
-                );
-            this.storeSort();
-        }
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    watch: {
-        blocks() {
-            this.initCurrentData();
-        },
-        blockList() {
-            if (this.keyboardSelected) {
-                this.$nextTick(() => {
-                    const selected = this.$refs['sortableHandle' + this.keyboardSelected][0];
-                    selected.focus();
-                    selected.scrollIntoView({behavior: "smooth", block: "center"});
-                });
-            }
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareLtiBlock.vue b/resources/vue/components/courseware/CoursewareLtiBlock.vue
deleted file mode 100644
index e967962..0000000
--- a/resources/vue/components/courseware/CoursewareLtiBlock.vue
+++ /dev/null
@@ -1,279 +0,0 @@
-<template>
-    <div class="cw-block cw-block-lti">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="false"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
-                <iframe
-                    v-if="toolId !== ''"
-                    class="cw-block-lti-content"
-                    :src="iframeUrl"
-                    :height="currentHeight"
-                    width="100%"
-                    allowfullscreen
-                    sandbox="allow-downloads allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts"
-                />
-                <div v-else class="cw-block-lti-content">
-                    <span class="cw-block-lti-icon-tool">
-                        {{ $gettext('Kein LTI-Tool konfiguriert') }}
-                    </span>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <courseware-tabs>
-                    <courseware-tab
-                        :index="0"
-                        :name="$gettext('Grunddaten')"
-                        :selected="true"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Titel') }}
-                                <input type="text" v-model="currentTitle" />
-                            </label>
-                            <label>
-                                {{ $gettext('Auswahl des externen Tools') }}
-                                <select v-model="currentToolId">
-                                    <option v-for="tool in tools" :key="tool.id" :value="tool.id">
-                                        {{ tool.name }}
-                                    </option>
-                                    <option value="0">{{ $gettext('Zugangsdaten selbst eingeben...') }}</option>
-                                </select>
-                            </label>
-                            <label v-show="allowCustomUrl">
-                                {{ $gettext('URL der Anwendung (optional)') }}
-                                <studip-tooltip-icon :text="$gettext('Sie können direkt auf eine URL in der Anwendung verlinken.')"/>
-                                <input type="text" v-model="currentLaunchUrl" :placeholder="currentTool?.launch_url" />
-                            </label>
-
-                            <div v-show="customToolSelected">
-                                <label class="studiprequired">
-                                    {{ $gettext('URL der Anwendung') }}
-                                    <span class="asterisk" :title="$gettext('Dies ist ein Pflichtfeld')" aria-hidden="true">*</span>
-                                    <studip-tooltip-icon :text="$gettext('Die Betreiber dieses Tools müssen Ihnen eine URL und Zugangsdaten (Consumer-Key und Consumer-Secret) mitteilen.')"/>
-                                    <input type="text" v-model="currentLaunchUrl" required>
-                                </label>
-                                <label class="studiprequired">
-                                    {{ $gettext('Consumer-Key des LTI-Tools') }}
-                                    <span class="asterisk" :title="$gettext('Dies ist ein Pflichtfeld')" aria-hidden="true">*</span>
-                                    <input type="text" v-model="currentConsumerKey" required>
-                                </label>
-                                <label class="studiprequired">
-                                    {{ $gettext('Consumer-Secret des LTI-Tools') }}
-                                    <span class="asterisk" :title="$gettext('Dies ist ein Pflichtfeld')" aria-hidden="true">*</span>
-                                    <input type="text" v-model="currentConsumerSecret" required>
-                                </label>
-                                <label>
-                                    {{ $gettext('OAuth Signatur Methode des LTI-Tools') }}
-                                    <select v-model="currentOauthSignatureMethod">
-                                        <option value="sha1">HMAC-SHA1</option>
-                                        <option value="sha256">HMAC-SHA256</option>
-                                    </select>
-                                </label>
-                                <label>
-                                    <input type="checkbox" v-model="currentSendLisPerson" />
-                                    {{ $gettext('Nutzerdaten an LTI-Tool senden') }}
-                                    <studip-tooltip-icon :text="$gettext('Nutzerdaten dürfen nur an das externe Tool gesendet werden, wenn es keine Datenschutzbedenken gibt. Mit Setzen des Hakens bestätigen Sie, dass die Übermittlung der Daten zulässig ist.')"/>
-                                </label>
-                            </div>
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="1"
-                        :name="$gettext('Zusätzliche Einstellungen')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Höhe') }}
-                                <input type="number" v-model="currentHeight" min="0" />
-                            </label>
-                            <label>
-                                {{ $gettext('Zusätzliche LTI-Parameter') }}
-                                <studip-tooltip-icon :text="$gettext('Ein Wert pro Zeile, Beispiel: Review:Chapter=1.2.56')"/>
-                                <textarea v-model="currentCustomParameters" />
-                            </label>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum LTI-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from "./CoursewareDefaultBlock.vue";
-import {blockMixin} from "./block-mixin";
-import {mapActions, mapGetters} from "vuex";
-import CoursewareTabs from "./CoursewareTabs.vue";
-import CoursewareTab from "./CoursewareTab.vue";
-
-export default {
-    name: 'courseware-lti-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareTab,
-        CoursewareTabs,
-        CoursewareDefaultBlock
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentHeight: '',
-            currentToolId: '',
-            currentLaunchUrl: '',
-            currentConsumerKey: '',
-            currentConsumerSecret: '',
-            currentOauthSignatureMethod: '',
-            currentSendLisPerson: false,
-            currentCustomParameters: '',
-        }
-    },
-    computed: {
-        ...mapGetters({
-            urlHelper: 'urlHelper',
-            ltiTools: 'lti-tools/all',
-        }),
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        height() {
-            return this.block?.attributes?.payload?.height;
-        },
-        tools() {
-            return this.ltiTools.map(tool => ({
-                id: tool.id,
-                name: tool.attributes.name,
-                launch_url: tool.attributes['launch-url'],
-                allow_custom_url: tool.attributes['allow-custom-url'],
-            }));
-        },
-        toolId() {
-            return this.block?.attributes?.payload?.tool_id;
-        },
-        currentTool() {
-            return this.tools.find(tool => tool.id === this.currentToolId);
-        },
-        allowCustomUrl() {
-            return this.currentTool?.allow_custom_url;
-        },
-        customToolSelected() {
-            return this.currentToolId === '0';
-        },
-        launchUrl() {
-            return this.block?.attributes?.payload?.launch_url;
-        },
-        consumerKey() {
-            return this.block?.attributes?.payload?.consumer_key;
-        },
-        consumerSecret() {
-            return this.block?.attributes?.payload?.consumer_secret;
-        },
-        oauthSignatureMethod() {
-            return this.block?.attributes?.payload?.oauth_signature_method ?? 'sha1';
-        },
-        sendLisPerson() {
-            return this.block?.attributes?.payload?.send_lis_person;
-        },
-        customParameters() {
-            return this.block?.attributes?.payload?.custom_parameters;
-        },
-        iframeUrl() {
-            return this.urlHelper.getURL('dispatch.php/courseware/lti/iframe/' + this.block.id);
-        },
-    },
-    async mounted() {
-        await this.loadLtiTools();
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            loadLtiTools: 'lti-tools/loadAll',
-            companionWarning: 'companionWarning',
-        }),
-        initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentHeight = this.height;
-            this.currentToolId = this.toolId !== '' ? this.toolId : this.currentToolId; // keep preselected tool
-            this.currentLaunchUrl = this.launchUrl;
-            this.currentConsumerKey = this.consumerKey;
-            this.currentConsumerSecret = this.consumerSecret;
-            this.currentOauthSignatureMethod = this.oauthSignatureMethod;
-            this.currentSendLisPerson = Boolean(this.sendLisPerson);  // prevent undefined value
-            this.currentCustomParameters = this.customParameters;
-        },
-        storeBlock() {
-            // require url, key and secret if custom tool is selected
-            if (this.currentToolId === '0') {
-                if (!this.currentLaunchUrl) {
-                    this.companionWarning({
-                        info: this.$gettext('Bitte geben Sie eine URL der Anwendung an.')
-                    });
-                    return false;
-                }
-                if (!this.currentConsumerKey) {
-                    this.companionWarning({
-                        info: this.$gettext('Bitte geben Sie den Consumer-Key des LTI-Tools an.')
-                    });
-                    return false;
-                }
-                if (!this.currentConsumerSecret) {
-                    this.companionWarning({
-                        info: this.$gettext('Bitte geben Sie den Consumer-Secret des LTI-Tools an.')
-                    });
-                    return false;
-                }
-            }
-
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.title = this.currentTitle;
-            attributes.payload.height = this.currentHeight;
-            attributes.payload.tool_id = this.currentToolId;
-            attributes.payload.launch_url = this.currentLaunchUrl;
-            if (this.currentToolId === '0') {
-                attributes.payload.consumer_key = this.currentConsumerKey;
-                attributes.payload.consumer_secret = this.currentConsumerSecret;
-                attributes.payload.oauth_signature_method = this.currentOauthSignatureMethod;
-                attributes.payload.send_lis_person = this.currentSendLisPerson;
-            }
-            attributes.payload.custom_parameters = this.currentCustomParameters;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-    watch: {
-        tools(value) {
-            // Preselect tool
-            if (this.currentToolId === '') {
-                if (value.length > 0) {
-                    // Preselect first tool
-                    this.currentToolId = value[0].id;
-                } else {
-                    // Preselect custom tool
-                    this.currentToolId = '0';
-                }
-            }
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewarePDFTableOfContent.vue b/resources/vue/components/courseware/CoursewarePDFTableOfContent.vue
deleted file mode 100644
index 989e2ed..0000000
--- a/resources/vue/components/courseware/CoursewarePDFTableOfContent.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<template>
-    <li class="cw-pdf-toc-item">
-        <a href="#" @click.prevent="tocPageNav(item.dest)">
-            <strong v-if="item.bold">{{ item.title }}</strong>
-            <span v-else>{{ item.title }}</span>
-        </a>
-        <template v-if="item.items">
-            <ul class="cw-pdf-toc-sub-list">
-                <courseware-pdf-toc-item v-for="(item, index) in item.items" :item="item" :key="index" @tocPageNav="tocPageNav"></courseware-pdf-toc-item>
-            </ul>
-        </template>
-    </li>
-</template>
-
-<script>
-export default {
-    name: 'courseware-pdf-toc-item',
-    props: {
-        item: {
-            type: Object,
-            required: true,
-        },
-    },
-    methods: {
-        tocPageNav(dest) {
-            this.$emit('tocPageNav', dest);
-        }
-    },
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareProgressCircle.vue b/resources/vue/components/courseware/CoursewareProgressCircle.vue
deleted file mode 100644
index e145590..0000000
--- a/resources/vue/components/courseware/CoursewareProgressCircle.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-<template>
-    <div class="cw-progress-circle" :class="['p' + value, value > 50 ? 'over50' : '']">
-        <span>{{ value }} %</span>
-        <div class="left-half-clipper">
-            <div class="first50-bar"></div>
-            <div class="value-bar"></div>
-        </div>
-    </div>
-</template>
-
-<script>
-export default {
-    name: 'courseware-progress-circle',
-    props: {
-        value: Number,
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareRibbon.vue b/resources/vue/components/courseware/CoursewareRibbon.vue
deleted file mode 100644
index ddf5c9b..0000000
--- a/resources/vue/components/courseware/CoursewareRibbon.vue
+++ /dev/null
@@ -1,170 +0,0 @@
-<template>
-    <div :class="{ 'cw-ribbon-wrapper-consume': consumeMode }" :id="isContentBar ? 'contentbar' : null" >
-        <div v-show="stickyRibbon" class="cw-ribbon-sticky-top"></div>
-        <header class="cw-ribbon" :class="{ 'cw-ribbon-sticky': stickyRibbon, 'cw-ribbon-consume': consumeMode }">
-            <div class="cw-ribbon-wrapper-left">
-                <nav class="cw-ribbon-nav" :class="buttonsClass">
-                    <slot name="buttons" />
-                </nav>
-                <nav class="cw-ribbon-breadcrumb">
-                    <ul>
-                        <slot v-if="breadcrumbFallback" name="breadcrumbFallback" />
-                        <slot v-else name="breadcrumbList" />
-                    </ul>
-                </nav>
-            </div>
-            <div class="cw-ribbon-wrapper-right">
-                <button
-                    v-if="showToolbarButton"
-                    class="cw-ribbon-button cw-ribbon-button-menu"
-                    :title="textRibbon.toolbar"
-                    @click.prevent="activeToolbar"
-                >
-                </button>
-                <slot name="menu" />
-            </div>
-            <div v-if="consumeMode" class="cw-ribbon-consume-bottom"></div>
-            <courseware-ribbon-toolbar
-                v-if="showTools"
-                :toolsActive="unfold"
-                :stickyRibbon="stickyRibbon"
-                :class="{ 'cw-ribbon-tools-sticky': stickyRibbon }"
-                :style="{ height: toolbarHeight + 'px' }"
-                :canEdit="canEdit"
-                @deactivate="deactivateToolbar"
-                @blockAdded="$emit('blockAdded')"
-            />
-        </header>
-        <div v-if="stickyRibbon" class="cw-ribbon-sticky-bottom"></div>
-        <div v-if="stickyRibbon" class="cw-ribbon-sticky-spacer"></div>
-    </div>
-</template>
-
-<script>
-import CoursewareRibbonToolbar from './CoursewareRibbonToolbar.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-ribbon',
-    components: {
-        CoursewareRibbonToolbar,
-    },
-    props: {
-        canEdit: Boolean,
-        showToolbarButton: {
-            default: true,
-            type: Boolean
-        },
-        showModeSwitchButton: {
-            default: true,
-            type: Boolean
-        },
-        buttonsClass: String,
-        isContentBar: {
-            type: Boolean,
-            default: false
-        }
-    },
-    data() {
-        return {
-            readModeActive: false,
-            stickyRibbon: false,
-            textRibbon: {
-                toolbar: this.$gettext('Inhaltsverzeichnis'),
-                fullscreen_on: this.$gettext('Fokusmodus einschalten'),
-                fullscreen_off: this.$gettext('Fokusmodus ausschalten'),
-            },
-            unfold: false,
-            showTools: false,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            consumeMode: 'consumeMode',
-            toolsActive: 'showToolbar'
-        }),
-        breadcrumbFallback() {
-            return window.outerWidth < 1200;
-        },
-        toolbarHeight() {
-            if (this.stickyRibbon) {
-                return parseInt(window.innerHeight * 0.75);
-            } else {
-                return parseInt(Math.min(window.innerHeight * 0.75, window.innerHeight - 197));
-            }
-        }
-    },
-    methods: {
-        ...mapActions({
-            coursewareConsumeMode: 'coursewareConsumeMode',
-            coursewareSelectedToolbarItem: 'coursewareSelectedToolbarItem',
-            coursewareViewMode: 'coursewareViewMode',
-            coursewareShowToolbar: 'coursewareShowToolbar'
-
-        }),
-        toggleConsumeMode() {
-            STUDIP.eventBus.emit('toggle-focus-mode', !this.consumeMode);
-            if (!this.consumeMode) {
-                document.body.classList.add('consuming_mode');
-                this.coursewareConsumeMode(true);
-                this.coursewareSelectedToolbarItem('contents');
-                this.coursewareViewMode('read');
-            } else {
-                this.coursewareConsumeMode(false);
-                document.body.classList.remove('consuming_mode');
-            }
-        },
-        activeToolbar() {
-            this.coursewareShowToolbar(true);
-        },
-        deactivateToolbar() {
-            this.coursewareShowToolbar(false);
-        },
-        handleScroll() {
-            if (window.outerWidth > 767) {
-                this.stickyRibbon = window.scrollY > 128 && !this.consumeMode;
-            } else {
-                this.stickyRibbon = window.scrollY > 75 && !this.consumeMode;
-            }
-        },
-    },
-    mounted() {
-        window.addEventListener('scroll', this.handleScroll);
-        if (this.isContentBar) {
-            STUDIP.eventBus.emit('courseware-contentbar-mounted', this);
-        }
-
-        this.globalOn('switch-focus-mode', (state) => {
-            if (state !== this.consumeMode) {
-                this.toggleConsumeMode();
-            }
-        });
-    },
-    beforeDestroy() {
-        STUDIP.eventBus.off('switch-focus-mode');
-    },
-    watch: {
-        toolsActive(newState, oldState) {
-            let view = this;
-            if(newState) {
-                this.showTools = true;
-                setTimeout(() => {view.unfold = true}, 10);
-            } else {
-                this.unfold = false;
-                setTimeout(() => {
-                    if(!view.toolsActive) {
-                        view.showTools = false;
-                    }
-                }, 800);
-            }
-        },
-        consumeMode(newState) {
-            if (newState) {
-                document.body.classList.add('consuming_mode');
-            } else {
-                document.body.classList.remove('consuming_mode');
-            }
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareRibbonToolbar.vue b/resources/vue/components/courseware/CoursewareRibbonToolbar.vue
deleted file mode 100644
index 795b6fa..0000000
--- a/resources/vue/components/courseware/CoursewareRibbonToolbar.vue
+++ /dev/null
@@ -1,206 +0,0 @@
-<template>
-    <focus-trap v-model="trap" :initial-focus="() => initialFocusElement" :clickOutsideDeactivates="true" :fallbackFocus ="() => fallbackFocusElement">
-        <div
-            class="cw-ribbon-tools"
-            :class="{ unfold: toolsActive, 'cw-ribbon-tools-consume': consumeMode }"
-        >
-            <div class="cw-ribbon-tool-content">
-                <div class="cw-ribbon-tool-content-nav">
-                    <courseware-tabs
-                        class="cw-ribbon-tool-content-tablist"
-                        ref="tabs"
-                        @selectTab="selectTool($event.alias)"
-                    >
-                        <courseware-tab
-                            :name="$gettext('Inhaltsverzeichnis')"
-                            :selected="showContents"
-                            alias="contents"
-                            ref="contents"
-                            :index="0"
-                        >
-                            <courseware-tools-contents
-                                id="cw-ribbon-tool-contents"
-                            />
-                        </courseware-tab>
-                        <courseware-tab
-                            v-if="displayAdder"
-                            :name="$gettext('Elemente hinzufügen')"
-                            :selected="showBlockAdder"
-                            alias="blockadder"
-                            class="cw-ribbon-tool-blockadder-tab"
-                            :index="1"
-                        >
-                            <courseware-tools-blockadder
-                                v-if="showBlockAdder"
-                                id="cw-ribbon-tool-blockadder"
-                                :stickyRibbon="stickyRibbon"
-                                @blockAdded="$emit('blockAdded')"
-                            />
-                        </courseware-tab>
-                    </courseware-tabs>
-                    <button
-                        :title="$gettext('schließen')"
-                        class="cw-tools-hide-button"
-                        ref="closeTools"
-                        @click="$emit('deactivate')">
-                    </button>
-                </div>
-            </div>
-        </div>
-    </focus-trap>
-</template>
-<script>
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import CoursewareToolsBlockadder from './CoursewareToolsBlockadder.vue';
-import CoursewareToolsContents from './CoursewareToolsContents.vue';
-import { FocusTrap } from 'focus-trap-vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-ribbon-toolbar',
-    components: {
-        CoursewareTabs,
-        CoursewareTab,
-        CoursewareToolsBlockadder,
-        CoursewareToolsContents,
-        FocusTrap,
-    },
-    props: {
-        toolsActive: Boolean,
-        canEdit: Boolean,
-        disableSettings: {
-            type: Boolean,
-            default: false,
-        },
-        disableAdder: {
-            type: Boolean,
-            default: false,
-        },
-        stickyRibbon: {
-            type: Boolean,
-            default: false,
-        },
-    },
-    data() {
-        return {
-            showContents: true,
-            showBlockAdder: false,
-            trap: false,
-            initialFocusElement: null
-        };
-    },
-    computed: {
-        ...mapGetters({
-            userIsTeacher: 'userIsTeacher',
-            consumeMode: 'consumeMode',
-            containerAdder: 'containerAdder',
-            adderStorage: 'blockAdder',
-            viewMode: 'viewMode',
-            context: 'context',
-            userById: 'users/byId',
-            userId: 'userId',
-            selectedToolbarItem: 'selectedToolbarItem',
-            currentElementisLink: 'currentElementisLink',
-        }),
-        showEditMode() {
-            return this.viewMode === 'edit';
-        },
-        displayAdder() {
-            if (this.disableAdder) {
-                return false;
-            } else {
-                return !this.consumeMode && this.showEditMode && this.canEdit && !this.currentElementisLink;
-            }
-        },
-        displaySettings() {
-            if (this.disableSettings) {
-                return false;
-            } else {
-                let user = this.userById({ id: this.userId });
-                return !this.consumeMode && this.context.type === 'courses' && (this.isTeacher || ['root', 'admin'].includes(user.attributes.permission));
-            }
-            
-        },
-        isTeacher() {
-            return this.userIsTeacher;
-        },
-        fallbackFocusElement(){
-            return this.$refs.contents;
-        }
-    },
-    methods: {
-        ...mapActions({
-            setToolbarItem: 'coursewareSelectedToolbarItem',
-            coursewareContainerAdder: 'coursewareContainerAdder'
-        }),
-        selectTool(alias) {
-            this.showContents = false;
-            this.showBlockAdder = false;
-
-            switch (alias) {
-                case 'contents':
-                    this.showContents = true;
-                    this.disableContainerAdder();
-                    this.scrollToCurrent();
-                    break;
-                case 'blockadder':
-                    this.showBlockAdder = true;
-                    break;
-            }
-
-            if (this.selectedToolbarItem !== alias) {
-                this.setToolbarItem(alias);
-            }
-        },
-        disableContainerAdder() {
-            if (this.containerAdder !== false) {
-                this.coursewareContainerAdder(false);
-            }
-        },
-        scrollToCurrent() {
-            setTimeout(() => {
-                let contents = this.$refs.contents.$el; 
-                let current = contents.querySelector('.cw-tree-item-link-current');
-                if (current) {
-                    contents.scroll({ top: current.offsetTop - 4, behavior: 'smooth' });
-                }
-            }, 360);
-        },
-    },
-    mounted () {
-        this.selectTool(this.selectedToolbarItem);
-    },
-    watch: {
-        adderStorage(newValue) {
-            if (Object.keys(newValue).length !== 0) {
-                this.selectTool('blockadder');
-            }
-        },
-        consumeMode(newValue) {
-            if (newValue) {
-                this.selectTool('contents');
-            }
-        },
-        containerAdder(newValue) {
-            if (newValue === true) {
-                this.selectTool('blockadder');
-            }
-        },
-        showEditMode(newValue) {
-            if (!newValue) {
-                this.selectTool('contents');
-            }
-        },
-        toolsActive(newValue) {
-            const focusElement = this.$refs.tabs.getTabButtonByAlias(this.selectedToolbarItem);
-            if (newValue && focusElement) {
-                setTimeout(() => {
-                    this.initialFocusElement = focusElement;
-                    this.trap = true;
-                }, 300);
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareSearchResults.vue b/resources/vue/components/courseware/CoursewareSearchResults.vue
deleted file mode 100644
index 9c9740d..0000000
--- a/resources/vue/components/courseware/CoursewareSearchResults.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<template>
-    <div role="region" id="search" aria-live="polite">
-        <courseware-ribbon
-            :showToolbarButton="false"
-            :showModeSwitchButton="false"
-            buttonsClass="single-icon"
-        >
-            <template #buttons>
-                <studip-icon shape="search" :size="24" />
-            </template>
-            <template #breadcrumbList>
-                <translate>Suchergebnisse</translate>
-            </template>
-            <template #menu>
-                <button :title="$gettext('Suchergebnisse schließen')" @click="closeResults">
-                    <studip-icon shape="decline" :size="24"/>
-                </button>
-            </template>
-        </courseware-ribbon>
-        <div id="search-results">
-            <article v-if="searchResults.length > 0">
-                <section v-for="result in searchResults" :key="result['structural-element-id']">
-                    <router-link
-                        :to="'/structural_element/' + result['structural-element-id']"
-                        @click.native="closeResults"
-                    >
-                        <div v-show="result.img !== null" class="search-result-img hidden-tiny-down">
-                            <img :src="result.img" />
-                        </div>
-                        <div class="search-result-data">
-                            <div class="search-result-title" v-html="result.name"></div>
-                            <div class="search-result-details">
-                                <div class="search-result-description" v-html="result.description"></div>
-                            </div>
-                        </div>
-                        <div class="search-result-information">
-                            <div class="search-result-time" v-html="result.date"></div>
-                        </div>
-                    </router-link>
-                </section>
-            </article>
-            <courseware-companion-box
-                v-else
-                :msgCompanion="$gettext('Es wurden keine Suchergebnisse gefunden.')"
-                mood="sad"
-            />
-        </div>
-    </div>
-</template>
-
-<script>
-import CoursewareRibbon from './CoursewareRibbon.vue';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import StudipIcon from '../StudipIcon.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-search-results',
-    components: {
-        CoursewareRibbon,
-        CoursewareCompanionBox,
-        StudipIcon
-    },
-    computed: {
-        ...mapGetters({
-            searchResults: 'searchResults'
-        }),
-    },
-    methods: {
-        ...mapActions({
-            setShowSearchResults: 'setShowSearchResults',
-            setSearchResults: 'setSearchResults',
-        }),
-        closeResults() {
-            this.setShowSearchResults(false);
-            this.setSearchResults([]);
-        },
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareSearchWidget.vue b/resources/vue/components/courseware/CoursewareSearchWidget.vue
deleted file mode 100644
index b8ebf0f..0000000
--- a/resources/vue/components/courseware/CoursewareSearchWidget.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-<template>
-   <sidebar-widget :title="$gettext('Suche')">
-        <template #content>
-            <form class="sidebar-search" @submit.prevent="">
-                <ul class="needles">
-                    <li>
-                        <div class="input-group files-search">
-                            <input
-                                type="text"
-                                v-model="searchTerm"
-                                :aria-label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')"
-                            />
-                            <a v-if="showSearchResults" @click.prevent="setShowSearchResults(false)"
-                                class="reset-search">
-                                <studip-icon shape="decline" :size="20"></studip-icon>
-                            </a>
-                            <button
-                                type="submit"
-                                :value="$gettext('Suchen')"
-                                aria-controls="search"
-                                class="submit-search"
-                                @click="loadResults"
-                            >
-                                <studip-icon shape="search" :size="20"></studip-icon>
-                            </button>
-                        </div>
-                    </li>
-                </ul>
-            </form>
-        </template>
-   </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-import StudipIcon from '../StudipIcon.vue';
-
-import { mapActions, mapGetters } from 'vuex';
-import axios from 'axios';
-
-export default {
-    name: 'courseware-search-widget',
-    components: {
-        StudipIcon,
-        SidebarWidget,
-    },
-    data() {
-        return {
-            searchTerm: ''
-        }
-    },
-    computed: {
-        ...mapGetters({
-            courseware: 'courseware',
-            context: 'context',
-            showSearchResults: 'showSearchResults'
-        }),
-    },
-    methods: {
-        ...mapActions({
-            setShowSearchResults: 'setShowSearchResults',
-            setSearchResults: 'setSearchResults',
-            companionWarning: 'companionWarning',
-            companionError: 'companionError'
-        }),
-        loadResults() {
-            if (this.searchTerm.length < 3) {
-                this.companionWarning({ info: this.$gettext('Leider ist Ihr Suchbegriff zu kurz. Der Suchbegriff muss mindestens 3 Zeichen lang sein.')});
-                return;
-            }
-            const limit = 100;
-            let params = {
-                search: this.searchTerm,
-                filters: { category: 'GlobalSearchCourseware', contextType: this.context.type, rangeId: this.context.id}
-            };
-
-            axios({
-                method: 'get',
-                url: STUDIP.URLHelper.getURL('dispatch.php/globalsearch/find/' + limit),
-                params: params,
-            }).then( result => {
-                this.setShowSearchResults(true);
-                if (result.data.GlobalSearchCourseware) {
-                    this.setSearchResults((result.data.GlobalSearchCourseware.content));
-                } else {
-                    this.setSearchResults([]);
-                }
-            }).catch(error => {
-                this.companionError({ info: this.$gettext('Bei der Anfrage ist ein Fehler aufgetreten.')});
-            });
-        }
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareSharedItems.vue b/resources/vue/components/courseware/CoursewareSharedItems.vue
deleted file mode 100644
index e68179c..0000000
--- a/resources/vue/components/courseware/CoursewareSharedItems.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<template>
-    <div class="cw-shared-items">
-        <h2 v-if="sharedElements.length > 0">{{ $gettext('Geteilte Lernmaterialien') }}</h2>
-        <ul class="cw-tiles">
-            <li
-                v-for="element in sharedElements"
-                :key="element.id"
-                class="tile"
-                :class="[element.attributes.payload.color, sharedElements.length > 3 ? '':  'cw-tile-margin']"
-            >
-                <a :href="getSharedElementUrl(element.id)" :title="element.attributes.title">
-                    <div
-                        class="preview-image"
-                        :class="[hasImage(element) ? '' : 'default-image']"
-                        :style="getChildStyle(element)"
-                    >
-                        <div class="overlay-text">{{ getOwnerName(element) }}</div>
-                    </div>
-                    <div class="description">
-                        <header
-                            :class="[element.attributes.purpose !== '' ? 'description-icon-' + element.attributes.purpose : '']"
-                        >
-                            {{ element.attributes.title }}
-                        </header>
-                        <div class="description-text-wrapper">
-                            <p>{{ element.attributes.payload.description }}</p>
-                        </div>
-                        <footer>
-                            {{ countChildren(element) + 1 }}
-                            <translate
-                                :translate-n="countChildren(element) + 1"
-                                translate-plural="Seiten"
-                            >
-                                Seite
-                            </translate>
-                        </footer>
-                    </div>
-                </a>
-            </li>
-        </ul>
-    </div>
-</template>
-<script>
-import { mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-shared-items',
-    computed: {
-        ...mapGetters({
-            sharedElements: 'courseware-structural-elements-shared/all',
-            userById: 'users/byId',
-        }),
-    },
-    methods: {
-        getChildStyle(child) {
-            let url = child.relationships?.image?.meta?.['download-url'];
-
-            if(url) {
-                return {'background-image': 'url(' + url + ')'};
-            } else {
-                return {};
-            }
-        },
-        hasImage(child) {
-            return child.relationships?.image?.data !== null;
-        },
-        getElementUrl(elementId) {
-            return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/courseware#/structural_element/' + elementId;
-        },
-        getSharedElementUrl(elementId) {
-            return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/shared_content_courseware/' + elementId;
-        },
-        getOwnerName(element) {
-            const ownerId = element.relationships.owner.data.id;
-            const owner = this.userById({ id: ownerId });
-
-            return owner.attributes['formatted-name']; 
-        },
-        countChildren(element) {
-            let data = element.relationships.children.data;
-            if (data) {
-                return data.length;
-            }
-            return 0;
-        },
-    },
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareShelfActionWidget.vue b/resources/vue/components/courseware/CoursewareShelfActionWidget.vue
deleted file mode 100644
index 27b4ab2..0000000
--- a/resources/vue/components/courseware/CoursewareShelfActionWidget.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<template>
-    <sidebar-widget id="courseware-action-widget"  :title="$gettext('Aktionen')">
-        <template #content>
-            <ul class="widget-list widget-links cw-action-widget">
-                <li class="cw-action-widget-add">
-                    <button @click="setShowUnitAddDialog(true)">
-                        {{ $gettext('Lernmaterial hinzufügen') }}
-                    </button>
-                </li>
-            </ul>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-shelf-action-widget',
-    components: {
-        SidebarWidget
-    },
-    methods: {
-        ...mapActions({
-            setShowUnitAddDialog: 'setShowUnitAddDialog',
-        }),
-    }
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareShelfDialogAdd.vue b/resources/vue/components/courseware/CoursewareShelfDialogAdd.vue
deleted file mode 100644
index 39bc47a..0000000
--- a/resources/vue/components/courseware/CoursewareShelfDialogAdd.vue
+++ /dev/null
@@ -1,317 +0,0 @@
-<template>
-    <studip-wizard-dialog
-        :title="$gettext('Lernmaterial hinzufügen')"
-        :confirmText="$gettext('Erstellen')"
-        :closeText="$gettext('Abbrechen')"
-        :slots="wizardSlots"
-        :lastRequiredSlotId="1"
-        :requirements="requirements"
-        @close="setShowUnitAddDialog(false)"
-        @confirm="createUnit"
-    >
-        <template v-slot:basic>
-            <form class="default" @submit.prevent="">
-                <label>
-                    <span>{{ text.title }}</span><span aria-hidden="true" class="wizard-required">*</span>
-                    <input type="text" v-model="addWizardData.title" required/>
-                </label>
-                <label>
-                    <span>{{ text.description }}</span><span aria-hidden="true" class="wizard-required">*</span>
-                    <textarea v-model="addWizardData.description" required/>
-                </label>
-            </form>
-        </template>
-        <template v-slot:layout>
-            <form class="default" @submit.prevent="">
-                <label>
-                        {{ $gettext('Bild hochladen') }}
-                        <br>
-                        <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile"/>
-                        <CoursewareCompanionBox
-                            v-if="uploadFileError"
-                            :msgCompanion="uploadFileError"
-                            mood="sad"
-                            class="cw-companion-box-in-form"
-                        />
-                </label>
-                <template v-if="selectedStockImage">
-                    <StockImageSelectableImageCard :stock-image="selectedStockImage" />
-                    <label>
-                        <button class="button" type="button" @click="selectedStockImage = null">
-                            {{ $gettext('Bild entfernen') }}
-                        </button>
-                    </label>
-                </template>
-                <label v-else>
-                    {{ $gettext('oder') }}
-                    <br>
-                    <button class="button" type="button" @click="showStockImageSelector = true">
-                        {{ $gettext('Aus dem Bilderpool auswählen') }}
-                    </button>
-                    <StockImageSelector v-if="showStockImageSelector" @close="showStockImageSelector = false" @select="onSelectStockImage" />
-                </label>
-                <label>
-                    {{ $gettext('Farbe') }}
-                    <studip-select
-                        v-model="addWizardData.color"
-                        :options="colors"
-                        :reduce="(color) => color.class"
-                        label="class"
-                    >
-                        <template #open-indicator="selectAttributes">
-                            <span v-bind="selectAttributes"
-                                ><studip-icon shape="arr_1down" :size="10"
-                            /></span>
-                        </template>
-                        <template #no-options>
-                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                        </template>
-                        <template #selected-option="{ name, hex }">
-                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                            ><span>{{ name }}</span>
-                        </template>
-                        <template #option="{ name, hex }">
-                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                            ><span>{{ name }}</span>
-                        </template>
-                    </studip-select>
-                </label>
-            </form>
-        </template>
-        <template v-slot:advanced>
-            <form class="default" @submit.prevent="">
-                <label>
-                    {{ $gettext('Art des Lernmaterials') }}
-                    <select v-model="addWizardData.purpose">
-                        <option value="content">{{ $gettext('Inhalt') }}</option>
-                        <option value="oer">{{ $gettext('OER-Material') }}</option>
-                        <option value="portfolio">{{ $gettext('ePortfolio') }}</option>
-                        <option value="draft">{{ $gettext('Entwurf') }}</option>
-                        <option v-if="!inCourseContext" value="template">{{ $gettext('Aufgabenvorlage') }}</option>
-                        <option value="other">{{ $gettext('Sonstiges') }}</option>
-                    </select>
-                </label>
-                <label>
-                    {{ $gettext('Lizenztyp') }}
-                    <select v-model="addWizardData.license_type">
-                        <option v-for="license in licenses" :key="license.id" :value="license.id">
-                            {{ license.name }}
-                        </option>
-                    </select>
-                </label>
-                <label>
-                    {{ $gettext('Geschätzter zeitlicher Aufwand') }}
-                    <input type="text" v-model="addWizardData.required_time" />
-                </label>
-                <label>
-                    {{ $gettext('Niveau') }}<br />
-                    {{ $gettext('von') }}
-                    <select v-model="addWizardData.difficulty_start">
-                        <option
-                            v-for="difficulty_start in 12"
-                            :key="difficulty_start"
-                            :value="difficulty_start"
-                        >
-                            {{ difficulty_start }}
-                        </option>
-                    </select>
-                    {{ $gettext('bis') }}
-                    <select v-model="addWizardData.difficulty_end">
-                        <option
-                            v-for="difficulty_end in 12"
-                            :key="difficulty_end"
-                            :value="difficulty_end"
-                        >
-                            {{ difficulty_end }}
-                        </option>
-                    </select>
-                </label>
-            </form>
-        </template>
-    </studip-wizard-dialog>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import StockImageSelectableImageCard from '../stock-images/SelectableImageCard.vue';
-import StockImageSelector from '../stock-images/SelectorDialog.vue';
-import StudipSelect from './../StudipSelect.vue';
-import StudipWizardDialog from './../StudipWizardDialog.vue';
-import colorMixin from '@/vue/mixins/courseware/colors.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-shelf-dialog-add',
-    mixins: [colorMixin],
-    components: {
-        CoursewareCompanionBox,
-        StockImageSelectableImageCard,
-        StockImageSelector,
-        StudipWizardDialog,
-        StudipSelect,
-    },
-    data() {
-        return {
-            wizardSlots: [
-                { id: 1, valid: false, name: 'basic', title: this.$gettext('Grundeinstellungen'), icon: 'courseware',
-                  description: this.$gettext('Wählen Sie einen kurzen, prägnanten Titel und beschreiben Sie in einigen Worten den Inhalt des Lernmaterials. Eine Beschreibung erleichtert Lernenden die Auswahl des Lernmaterials.') },
-                { id: 2, valid: true, name: 'layout', title: this.$gettext('Erscheinung'), icon: 'picture',
-                  description: this.$gettext('Ein Vorschaubild motiviert Lernende das Lernmaterial zu erkunden. Die Kombination aus Bild und Farbe erleichtert das wiederfinden des Lernmaterials in der Übersicht.') },
-                { id: 3, valid: true, name: 'advanced', title: this.$gettext('Zusatzangaben'), icon: 'info-list',
-                  description: this.$gettext('Hier können Sie detaillierte Angaben zum Lernmaterial eintragen. Diese sind besonders interessant wenn das Lernmaterial als OER geteilt wird.') }
-            ],
-            text: {
-                title: this.$gettext('Titel des Lernmaterials'),
-                description: this.$gettext('Beschreibung')
-            },
-            addWizardData: {},
-            uploadFileError: '',
-            requirements: [],
-            showStockImageSelector: false,
-            selectedStockImage: null,
-        }
-    },
-    computed: {
-        ...mapGetters({
-            licenses: 'licenses',
-            context: 'context',
-            lastCreateCoursewareUnit: 'courseware-units/lastCreated',
-            structuralElementById:  'courseware-structural-elements/byId',
-        }),
-        inCourseContext() {
-            return this.context.type === 'courses';
-        },
-        colors() {
-            return this.mixinColors.filter(color => color.darkmode);
-        }
-    },
-    mounted() {
-        this.initAddWizardData();
-    },
-    methods: {
-        ...mapActions({
-            companionError: 'companionError',
-            companionInfo: 'companionInfo',
-            companionSuccess: 'companionSuccess',
-            createCoursewareUnit: 'courseware-units/create',
-            setShowUnitAddDialog: 'setShowUnitAddDialog',
-            setStockImageForStructuralElement: 'setStockImageForStructuralElement',
-            loadStructuralElementById: 'courseware-structural-elements/loadById',
-            uploadImageForStructuralElement: 'uploadImageForStructuralElement',
-        }),
-        initAddWizardData() {
-            this.addWizardData = {
-                title: '',
-                description: '',
-                purpose: 'content',
-                color: 'studip-blue',
-            }
-        },
-        validateSlots() {
-            let valid = true;
-            this.wizardSlots.forEach(slot => {
-                if (!slot.valid) {
-                    valid = false;
-                }
-            });
-
-            return valid;
-        },
-        checkUploadFile() {
-            const file = this.$refs?.upload_image?.files[0];
-            if (file.size > 2097152) {
-                this.uploadFileError = this.$gettext('Diese Datei ist zu groß. Bitte wählen Sie eine Datei aus, die kleiner als 2MB ist.');
-            } else if (!file.type.includes('image')) {
-                this.uploadFileError = this.$gettext('Diese Datei ist kein Bild. Bitte wählen Sie ein Bild aus.');
-            } else {
-                this.uploadFileError = '';
-                this.selectedStockImage = null;
-            }
-        },
-        async createUnit() {
-            if (!this.validateSlots()) {
-                this.companionError({
-                    info: this.$gettext('Bitte füllen Sie alle notwendigen Angaben aus.'),
-                });
-                return false;
-            }
-            const file = this.$refs?.upload_image?.files[0];
-            const unit = {
-                attributes: {
-                    title: this.addWizardData.title,
-                    purpose: this.addWizardData.purpose,
-                    payload: {
-                        description: this.addWizardData.description,
-                        color: this.addWizardData.color,
-                        license_type: this.addWizardData.license_type,
-                        required_time: this.addWizardData.required_time,
-                        difficulty_start: this.addWizardData.difficulty_start,
-                        difficulty_end: this.addWizardData.difficulty_end
-                    }
-                },
-                relationships: {
-                    range: {
-                        data: {
-                            type: this.context.type,
-                            id: this.context.id
-                        }
-                    }
-                }
-            };
-            this.setShowUnitAddDialog(false);
-
-            await this.createCoursewareUnit(unit, { root: true });
-            this.companionSuccess({ info: this.$gettext('Neues Lernmaterial angelegt.') });
-            const newElementId = this.lastCreateCoursewareUnit.relationships['structural-element'].data.id
-            await this.loadStructuralElementById({ id: newElementId });
-            let newStructuralElement = this.structuralElementById({id: newElementId});
-
-            try {
-                if (file) {
-                    await this.uploadImageForStructuralElement({
-                        structuralElement: newStructuralElement,
-                        file,
-                    });
-                } else if (this.selectedStockImage) {
-                    await this.setStockImageForStructuralElement({
-                        structuralElement: newStructuralElement,
-                        stockImage: this.selectedStockImage,
-                    })
-                }
-
-                this.loadStructuralElementById({id: newStructuralElement.id, options: {include: 'children'}});
-            } catch(error) {
-                console.error(error);
-                this.companionError({ info: this.$gettext('Das Bild für das neue Lernmaterial konnte nicht gespeichert werden.') });
-            }
-        },
-        onSelectStockImage(stockImage) {
-            if (this.$refs?.upload_image) {
-                this.$refs.upload_image.value = null;
-            }
-            this.selectedStockImage = stockImage;
-            this.showStockImageSelector = false;
-        },
-    },
-    watch: {
-        addWizardData: {
-            handler(newData) {
-                this.requirements = [];
-                const slot = this.wizardSlots[0];
-                if (newData.title !== '' && newData.description !== '') {
-                    slot.valid = true;
-                }
-                if (newData.title === '' ) {
-                    slot.valid = false;
-                    this.requirements.push({slot: slot, text: this.text.title });
-                }
-                if (newData.description === '') {
-                    slot.valid = false;
-                    this.requirements.push({slot:  slot, text: this.text.description });
-                }
-            },
-            deep: true
-        }
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareShelfDialogCopy.vue b/resources/vue/components/courseware/CoursewareShelfDialogCopy.vue
deleted file mode 100644
index d9ab4ff..0000000
--- a/resources/vue/components/courseware/CoursewareShelfDialogCopy.vue
+++ /dev/null
@@ -1,401 +0,0 @@
-<template>
-    <studip-wizard-dialog
-        :title="$gettext('Lernmaterial kopieren')"
-        :confirmText="$gettext('Kopieren')"
-        :closeText="$gettext('Abbrechen')"
-        :lastRequiredSlotId="2"
-        :requirements="requirements"
-        :slots="wizardSlots"
-        @close="close"
-        @confirm="copy"
-    >
-        <template v-slot:source>
-            <form class="default" @submit.prevent="">
-                <fieldset class="radiobutton-set">
-                    <template v-if="inCourseContext">
-                        <input
-                            id="cw-shelf-copy-source-self"
-                            type="radio"
-                            v-model="source"
-                            value="self"
-                            :aria-description="text.sourceSelf"
-                        />
-                        <label @click="source = 'self'" for="cw-shelf-copy-source-self">
-                            <div class="icon"><studip-icon shape="seminar" size="32"/></div>
-                            <div class="text">{{ text.sourceSelf }}</div>
-                            <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
-                            <studip-icon shape="check-circle" size="24" class="check" />
-                        </label>
-                    </template>
-                    <input
-                        id="cw-shelf-copy-source-courses"
-                        type="radio"
-                        v-model="source"
-                        value="courses"
-                        :aria-description="text.sourceCourses"
-                    />
-                    <label @click="source = 'courses'" for="cw-shelf-copy-source-courses">
-                        <div class="icon"><studip-icon shape="seminar" size="32"/></div>
-                        <div class="text">{{ text.sourceCourses }}</div>
-                        <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
-                        <studip-icon shape="check-circle" size="24" class="check" />
-                    </label>
-                    <input
-                        id="cw-shelf-copy-source-users"
-                        type="radio"
-                        v-model="source"
-                        value="users"
-                        :aria-description="text.sourceUsers"
-                    />
-                    <label @click="source = 'users'" for="cw-shelf-copy-source-users">
-                        <div class="icon"><studip-icon shape="content" size="32"/></div>
-                        <div class="text">{{ text.sourceUsers }}</div>
-                        <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
-                        <studip-icon shape="check-circle" size="24" class="check" />
-                    </label>
-                </fieldset>
-                <template v-if="source === 'courses'">
-                    <label>
-                        <span>{{ $gettext('Semester') }}</span><span aria-hidden="true"></span>
-                        <select v-model="selectedSemester">
-                            <option value="all">{{ $gettext('Alle Semester') }}</option>
-                            <option v-for="semester in semesterMap" :key="semester.id" :value="semester.id">
-                                {{ semester.attributes.title }}
-                            </option>
-                        </select>
-                    </label>
-                    <label>
-                        <span>{{ $gettext('Veranstaltung') }}</span><span aria-hidden="true" class="wizard-required">*</span>
-                        <studip-select
-                            v-if="filteredCourses.length !== 0 && !loadingCourses"
-                            :options="filteredCourses"
-                            :clearable="false"
-                            :reduce="option => option.id"
-                            :getOptionLabel="option => option.attributes.title"
-                            v-model="selectedRange"
-                        >
-                            <template #open-indicator="selectAttributes">
-                                <span v-bind="selectAttributes"
-                                    ><studip-icon shape="arr_1down" size="10"
-                                /></span>
-                            </template>
-                        </studip-select>
-                        <p v-if="loadingCourses">
-                            {{$gettext('Lade Veranstaltungen…')}}
-                        </p>
-                        <p v-if="filteredCourses.length === 0 && !loadingCourses">
-                            {{$gettext('Es wurden keine geeigneten Veranstaltungen gefunden.')}}
-                        </p>
-                    </label>
-                </template>
-
-            </form>
-        </template>
-        <template v-slot:unit>
-            <form class="default" @submit.prevent="">
-                <fieldset v-if="units.length !== 0" class="radiobutton-set">
-                    <template v-for="unit in units">
-                        <input
-                            :id="'cw-shelf-copy-unit-' + unit.id"
-                            type="radio"
-                            v-model="selectedUnit"
-                            :checked="unit.id === selectedUnitId"
-                            :value="unit"
-                            :key="'radio-' + unit.id"
-                            :aria-description="unit.element.attributes.title"
-                        />
-                        <label @click="selectedUnit = unit" :key="'label-' + unit.id" :for="'cw-shelf-copy-unit-' + unit.id">
-                            <div class="icon"><studip-icon shape="courseware" size="32"/></div>
-                            <div class="text">{{ unit.element.attributes.title }}</div>
-                            <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
-                            <studip-icon shape="check-circle" size="24" class="check" />
-                        </label>
-                    </template>
-                </fieldset>
-                <courseware-companion-box
-                    v-else
-                    mood="sad"
-                    :msgCompanion="$gettext('Für die gewählte Quelle stehen keine Lernmaterialien zur Verfügung.')"
-                />
-            </form>
-        </template>
-        <template v-slot:edit>
-            <form v-if="selectedUnit" class="default" @submit.prevent="">
-                <label>
-                    <span>{{$gettext('Titel')}}</span><span aria-hidden="true" class="wizard-required">*</span>
-                    <input type="text" v-model="modifiedTitle" :placeholder="selectedUnitTitle" required />
-                </label>
-                <label>
-                    {{$gettext('Farbe')}}
-                    <studip-select
-                        v-model="modifiedColor"
-                        :options="colors"
-                        :reduce="(color) => color.class"
-                        :clearable="false"
-                        label="class"
-                    >
-                        <template #open-indicator="selectAttributes">
-                            <span v-bind="selectAttributes"
-                                ><studip-icon shape="arr_1down" size="10"
-                            /></span>
-                        </template>
-                        <template #no-options>
-                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                        </template>
-                        <template #selected-option="{ name, hex }">
-                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                            ><span>{{ name }}</span>
-                        </template>
-                        <template #option="{ name, hex }">
-                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                            ><span>{{ name }}</span>
-                        </template>
-                    </studip-select>
-                </label>
-                <label>
-                    <span>{{$gettext('Beschreibung')}}</span><span aria-hidden="true" class="wizard-required">*</span>
-                    <textarea v-model="modifiedDescription" :placeholder="selectedUnitDescription" required />
-                </label>
-            </form>
-            <courseware-companion-box
-                    v-else
-                    mood="pointing"
-                    :msgCompanion="$gettext('Bitte wählen Sie ein Lernmaterial aus.')"
-                />
-        </template>
-    </studip-wizard-dialog>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import colorMixin from '@/vue/mixins/courseware/colors.js';
-import StudipSelect from './../StudipSelect.vue';
-import StudipWizardDialog from './../StudipWizardDialog.vue';
-
-import { mapActions, mapGetters } from 'vuex'
-
-export default {
-    name: 'courseware-shelf-dialog-copy',
-    mixins: [colorMixin],
-    components: {
-        CoursewareCompanionBox,
-        StudipWizardDialog,
-        StudipSelect,
-    },
-    data() {
-        return {
-            wizardSlots: [
-                { id: 1, valid: false, name: 'source', title: this.$gettext('Quelle'), icon: 'source',
-                  description: this.$gettext('Wählen Sie hier den Ort in Stud.IP aus, an dem sich das zu kopierende Lernmaterial befindet.') },
-                { id: 2, valid: false, name: 'unit', title: this.$gettext('Lernmaterial'), icon: 'courseware',
-                  description: this.$gettext('Wählen Sie hier das gewünschte Lernmaterial aus der Liste aus. Eine Auswahl wird durch einen grauen Hintergrund und einen Kontrollhaken angezeigt.') },
-                { id: 3, valid: true, name: 'edit', title: this.$gettext('Anpassen'), icon: 'edit',
-                  description: this.$gettext('Sie können hier die Daten des zu kopierenden Lernmaterials anpassen. Eine Anpassung ist optional, Sie können das Lernmaterial auch unverändert kopieren.') },
-            ],
-            source: '',
-            loadingCourses: false,
-            courses: [],
-            semesterMap: [],
-            selectedSemester: 'all',
-            selectedRange: '',
-            loadingUnits: false,
-            selectedUnit: null,
-            selectedUnitElement: null,
-            modifiedTitle: '',
-            modifiedColor: '',
-            modifiedDescription: '',
-
-            requirements: [],
-            text: {
-                source: this.$gettext('Quelle'),
-                unit: this.$gettext('Lernmaterial'),
-                sourceSelf: this.$gettext('Diese Veranstaltung'),
-                sourceCourses: this.$gettext('Veranstaltung'),
-                sourceUsers: this.$gettext('Arbeitsplatz'),
-
-            }
-        }
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            coursewareUnits: 'courseware-units/all',
-            semesterById: 'semesters/byId',
-            structuralElementById: 'courseware-structural-elements/byId',
-            context: 'context'
-        }),
-        colors() {
-            return this.mixinColors.filter(color => color.darkmode);
-        },
-        units() {
-            let units = this.coursewareUnits.filter(unit => unit.relationships.range.data.id === this.selectedRange);
-            units.forEach(unit => {
-                unit.element = this.getUnitElement(unit);
-            });
-
-            if (this.inCourseContext) {
-                units = units.filter(unit => unit.element.attributes.purpose !== 'template');
-            }
-
-            return units;
-        },
-        selectedUnitId() {
-            return this.selectedUnit?.id;
-        },
-        inCourseContext() {
-            return this.context.type === 'courses';
-        },
-        selectedUnitTitle() {
-            return this.selectedUnitElement.attributes.title ?? '';
-        },
-        selectedUnitDescription() {
-            return this.selectedUnitElement.attributes.payload.description ?? '';
-        },
-        filteredCourses() {
-            const courses = this.courses.filter((course) => { return course.id !== this.context.id });
-            if (this.selectedSemester === 'all') {
-                return courses;
-            } else {
-                return courses.filter((course) => {
-                    return course.relationships['start-semester'].data.id === this.selectedSemester;
-                });
-            }
-        }
-    },
-    async mounted() {
-        this.initWizardData();
-    },
-    methods: {
-        ...mapActions({
-            companionSuccess: 'companionSuccess',
-            loadCourseUnits: 'loadCourseUnits',
-            loadUsersCourses: 'loadUsersCourses',
-            loadSemester: 'semesters/loadById',
-            loadUserUnits: 'loadUserUnits',
-            setShowUnitCopyDialog: 'setShowUnitCopyDialog',
-            copyUnit: 'copyUnit',
-        }),
-        initWizardData() {
-            this.source = this.inCourseContext ? 'self' : 'users';
-            this.selectedRange = '';
-            this.selectedUnit = null;
-        },
-        close() {
-            this.setShowUnitCopyDialog(false);
-            this.initWizardData();
-        },
-        getUnitElement(unit) {
-            return this.structuralElementById({id: unit.relationships['structural-element'].data.id});
-        },
-        async copy() {
-            if (this.selectedUnit) {
-                const element = this.getUnitElement(this.selectedUnit);
-                const modified = {
-                        title: this.modifiedTitle !== '' ? this.modifiedTitle : this.selectedUnitTitle,
-                        color: this.modifiedColor,
-                        description: this.modifiedDescription !== '' ? this.modifiedDescription : this.selectedUnitDescription
-                }
-                await this.copyUnit({ unitId: this.selectedUnit.id, modified: modified });
-                this.companionSuccess({ info: this.$gettext('Lernmaterial kopiert.') });
-                this.close();
-            }
-        },
-        async updateCourses() {
-            this.loadingCourses = true;
-            this.courses = await this.loadUsersCourses({ userId: this.userId, withCourseware: true });
-            this.loadSemesterMap();
-            this.loadingCourses = false;
-        },
-        loadSemesterMap() {
-            let view = this;
-            let semesters = [];
-            this.courses.every(course => {
-                let semId = course.relationships['start-semester'].data.id;
-                if(!semesters.includes(semId)) {
-                    semesters.push(semId);
-                }
-                return true;
-            });
-            semesters.every(semester => {
-                view.loadSemester({id: semester}).then( () => {
-                    view.semesterMap.push(view.semesterById({id: semester}));
-                    view.semesterMap.sort((a, b) => new Date(b.attributes.start) - new Date(a.attributes.start));
-                });
-                return true;
-            });
-        },
-        async updateCourseUnits(cid) {
-            this.loadingUnits = true;
-            await this.loadCourseUnits(cid);
-            this.loadingUnits = false;
-        },
-        setElementData() {
-            this.selectedUnitElement = this.getUnitElement(this.selectedUnit);
-            this.modifiedTitle = this.selectedUnitElement.attributes.title;
-            this.modifiedColor = this.selectedUnitElement.attributes.payload.color;
-            this.modifiedDescription = this.selectedUnitElement.attributes.payload.description;
-        },
-        resetElementData() {
-            this.modifiedTitle = '';
-            this.modifiedColor = '';
-            this.modifiedDescription = '';
-        },
-        validateSelection() {
-            this.requirements = [];
-            if (this.selectedRange === '') {
-                this.requirements.push({slot: this.wizardSlots[0], text: this.text.source });
-            }
-            if (this.selectedUnit === null) {
-                this.requirements.push({slot: this.wizardSlots[1], text: this.text.unit });
-            }
-        }
-    },
-    watch: {
-        selectedUnit(newUnit) {
-            this.validateSelection();
-            const slot = this.wizardSlots[1];
-            if (newUnit !== null) {
-                slot.valid = true;
-                this.setElementData();
-            } else {
-                slot.valid = false;
-                this.resetElementData();
-            }
-        },
-        selectedRange(newRid) {
-            this.selectedUnit = null;
-            this.validateSelection();
-            const slot = this.wizardSlots[0];
-            
-            if (newRid !== '') {
-                slot.valid = true;
-                if (this.source === 'courses' || this.source === 'self') {
-                    this.updateCourseUnits(newRid);
-                }
-                if (this.source === 'users') {
-                    this.loadUserUnits(newRid);
-                }
-            } else {
-                slot.valid = false;
-            }
-        },
-        source(newSource) {
-            switch (newSource) {
-                case 'self':
-                    this.selectedRange = this.context.id;
-                    break;
-                case 'courses':
-                    this.selectedRange = '';
-                    this.updateCourses();
-                    break;
-                case 'users':
-                    this.selectedRange = this.userId;
-                    break;
-            }
-        },
-        selectedSemester(newSemester) {
-            this.selectedRange = '';
-        }
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareShelfDialogImport.vue b/resources/vue/components/courseware/CoursewareShelfDialogImport.vue
deleted file mode 100644
index cc8e100..0000000
--- a/resources/vue/components/courseware/CoursewareShelfDialogImport.vue
+++ /dev/null
@@ -1,373 +0,0 @@
-<template>
-    <div class="cw-shelf-dialog-import-wrapper">
-        <studip-wizard-dialog
-            v-if="!importRunning"
-            :title="$gettext('Lernmaterial importieren')"
-            :confirmText="$gettext('Importieren')"
-            :closeText="$gettext('Abbrechen')"
-            :slots="wizardSlots"
-            :lastRequiredSlotId="1"
-            :requirements="requirements"
-            @close="setShowUnitImportDialog(false)"
-            @confirm="importCoursewareArchiv"
-        >
-            <template v-slot:file>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        <span>{{ text.import }}</span><span aria-hidden="true" class="wizard-required">*</span>
-                        <input v-show="importZipFile === null" ref="fileInput" class="cw-file-input" type="file" accept=".zip" @change="setImport" />
-                        <p v-show="importZipFile !== null" class="cw-file-input-change">
-                            <button class="button" @click="$refs.fileInput.click()">{{ $gettext('Datei ändern')}}</button><span>{{ importZipFile?.name }}</span>
-                        </p>
-                    </label>
-                    <fieldset v-show="archiveErrors.length > 0">
-                        <legend>{{$gettext('Fehler im Import-Archiv')}}</legend>
-                        <ul>
-                            <li v-for="(error, index) in archiveErrors" :key="index"> {{error}} </li>
-                        </ul>
-                    </fieldset>
-                </form>
-            </template>
-            <template v-slot:edit>
-                <form v-if="hasValidFile" class="default" @submit.prevent="">
-                    <label>
-                        {{ text.title }}
-                        <input type="text" v-model="modifiedData.title" :placeholder="loadedTitle" required />
-                    </label>
-                    <label>
-                        {{ text.color }}
-                        <studip-select
-                            v-model="modifiedData.color"
-                            :options="colors"
-                            :reduce="(color) => color.class"
-                            :clearable="false"
-                            label="class"
-                        >
-                            <template #open-indicator="selectAttributes">
-                                <span v-bind="selectAttributes"
-                                    ><studip-icon shape="arr_1down" size="10"
-                                /></span>
-                            </template>
-                            <template #no-options>
-                                {{$gettext('Es steht keine Auswahl zur Verfügung.')}}
-                            </template>
-                            <template #selected-option="{ name, hex }">
-                                <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                                ><span>{{ name }}</span>
-                            </template>
-                            <template #option="{ name, hex }">
-                                <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                                ><span>{{ name }}</span>
-                            </template>
-                        </studip-select>
-                    </label>
-                    <label>
-                        {{ text.description }}
-                        <textarea v-model="modifiedData.description" :placeholder="loadedDescription" required />
-                    </label>
-                </form>
-                <courseware-companion-box 
-                    v-else
-                    :msgCompanion="$gettext('Bitte wählen Sie ein Import-Archiv aus.')"
-                    mood="unsure"
-                />
-            </template>
-        </studip-wizard-dialog>
-        <studip-dialog
-            v-if="importRunning"
-            :title="$gettext('Lernmaterial importieren')"
-            :closeText="$gettext('Schließen')"
-            height="420"
-            @close="setShowUnitImportDialog(false)"
-        >
-            <template v-slot:dialogContent>
-                <div role="status" aria-live="polite">
-                    <courseware-companion-box 
-                        v-show="importDone && importErrors.length === 0"
-                        :msgCompanion="$gettext('Import erfolgreich!')"
-                        mood="special"
-                    />
-                    <courseware-companion-box
-                        v-show="importDone && importErrors.length > 0"
-                        :msgCompanion="$gettext('Import abgeschlossen. Es sind Fehler aufgetreten!')"
-                        mood="unsure"
-                    />
-                    <courseware-companion-box
-                        v-show="!importDone"
-                        :msgCompanion="$gettext('Import läuft. Bitte schließen Sie den Dialog nicht bis der Import abgeschlossen wurde.')"
-                        mood="pointing"
-                    />
-                </div>
-                <form v-if="!importDone" class="default" @submit.prevent="">
-                    <fieldset>
-                        <div v-if="!fileImportDone" class="cw-import-zip">
-                            <header>{{$gettext('Importiere Dateien')}}:</header>
-                            <div class="progress-bar-wrapper">
-                                <div class="progress-bar" role="progressbar" :style="{width: importFilesProgress + '%'}" :aria-valuenow="importFilesProgress" aria-valuemin="0" aria-valuemax="100">{{ importFilesProgress }}%</div>
-                            </div>
-                            {{ importFilesState }}
-                        </div>
-                        <div v-if="fileImportDone" class="cw-import-zip">
-                            <header>{{$gettext('Importiere Elemente')}}:</header>
-                            <div class="progress-bar-wrapper">
-                                <div class="progress-bar" role="progressbar" :style="{width: importStructuresProgress + '%'}" :aria-valuenow="importStructuresProgress" aria-valuemin="0" aria-valuemax="100">{{ importStructuresProgress }}%</div>
-                            </div>
-                            {{ importStructuresState }}
-                        </div>
-                    </fieldset>
-                </form>
-                <form v-if="importErrors.length > 0" class="default" @submit.prevent="">
-                    <fieldset>
-                        <legend>{{$gettext('Fehlermeldungen')}}</legend>
-                        <ul>
-                            <li v-for="(error, index) in importErrors" :key="index"> {{error}} </li>
-                        </ul>
-                    </fieldset>
-                </form>
-            </template>
-        </studip-dialog>
-    </div>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareImport from '@/vue/mixins/courseware/import.js';
-import colorMixin from '@/vue/mixins/courseware/colors.js';
-import StudipWizardDialog from './../StudipWizardDialog.vue';
-
-import { mapActions, mapGetters } from 'vuex'
-import JSZip from 'jszip';
-
-export default {
-    name: 'courseware-shelf-dialog-import',
-    components: {
-        StudipWizardDialog,
-        CoursewareCompanionBox
-    },
-    mixins: [CoursewareImport, colorMixin],
-    data() {
-        return {
-            wizardSlots: [
-                { id: 1, valid: false, name: 'file', title: this.$gettext('Import-Archiv'), icon: 'file-archive',
-                  description: this.$gettext('Wählen Sie hier eine Courseware-Export-Archiv-Datei von Ihrer Festplatte aus. Bei Courseware-Export-Archiven handelt es sich um Zip-Dateien. Diese sollten mindestens die Dateien files.json und courseware.json enthalten.') },
-                { id: 2, valid: true, name: 'edit', title: this.$gettext('Anpassen'), icon: 'edit', description: this.$gettext('Sie können hier die Daten des zu importierenden Lernmaterials anpassen. Eine Anpassung ist optional, Sie können das Archiv auch unverändert importieren.') },
-            ],
-            modifiedData: {
-                title: '',
-                color: 'studip-blue',
-                description: ''
-            },
-            importArchivFile: null,
-            importRunning: false,
-            importZipFile: null,
-            zip: null,
-
-            loadedZipData: null,
-            archiveErrors: [],
-
-            requirements: [],
-            text: {
-                import: this.$gettext('Importdatei'),
-                title: this.$gettext('Titel'),
-                color: this.$gettext('Farbe'),
-                description: this.$gettext('Beschreibung'),
-            }
-        }
-    },
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            importFilesState: 'importFilesState',
-            importFilesProgress: 'importFilesProgress',
-            importStructuresState: 'importStructuresState',
-            importStructuresProgress: 'importStructuresProgress',
-            importErrors: 'importErrors',
-            lastCreateCoursewareUnit: 'courseware-units/lastCreated',
-            
-        }),
-        colors() {
-            return this.mixinColors.filter(color => color.darkmode);
-        },
-        fileImportDone() {
-            return this.importFilesProgress === 100;
-        },
-        importDone() {
-            return this.importFilesProgress === 100 && this.importStructuresProgress === 100;
-        },
-        hasValidFile() {
-            return this.archiveErrors.length === 0 && this.loadedZipData !== null;
-        },
-        loadedTitle() {
-            return this.loadedZipData.courseware.attributes.title ?? '';
-        },
-        loadedDescription() {
-            return this.loadedZipData.courseware.attributes.payload.description ?? '';
-        }
-    },
-    methods: {
-        ...mapActions({
-            setShowUnitImportDialog: 'setShowUnitImportDialog',
-            createCoursewareUnit: 'courseware-units/create',
-            setImportFilesProgress: 'setImportFilesProgress',
-            setImportStructuresProgress: 'setImportStructuresProgress',
-            setImportErrors: 'setImportErrors',
-            loadStructuralElementById: 'courseware-structural-elements/loadById',
-            companionSuccess: 'companionSuccess',
-        }),
-        setImport(event) {
-            this.importZipFile = event.target.files[0];
-            this.loadZipData(); 
-        },
-
-        async loadZipData() {
-            const slot = this.wizardSlots[0];
-            const text = this.text.import;
-            this.archiveErrors = [];
-            this.loadedZipData = null;
-            this.modifiedData.title = '';
-            this.modifiedData.color = 'studip-blue';
-            this.modifiedData.description = '';
-            let filesError = false;
-            if (!this.importZipFile.type.includes('zip')) {
-                this.archiveErrors.push(this.$gettext('Die gewählte Datei ist kein Archiv.'));
-                filesError = true;
-            }
-            if (!filesError) {
-                try {
-                    this.zip = await JSZip.loadAsync(this.importZipFile);
-                } catch(error) {
-                    this.zip = null;
-                    this.archiveErrors.push(this.$gettext('Beim laden des Archivs ist ein Fehler aufgetreten. Vermutlich ist das Archiv beschädigt.'));
-                    filesError = true;
-                }
-                
-                if (this.zip) {
-                    if (this.zip.file('courseware.json') === null) {
-                        this.archiveErrors.push(this.$gettext('Das Archiv enthält keine courseware.json Datei.'));
-                        filesError = true;
-                    }
-                    if (this.zip.file('files.json') === null) {
-                        this.archiveErrors.push(this.$gettext('Das Archiv enthält keine files.json Datei.'));
-                        filesError = true;
-                    }
-                    if (this.zip.file('data.xml') !== null) {
-                        this.archiveErrors.push(this.$gettext(
-                            'Das Archiv enthält eine data.xml Datei. Möglicherweise handelt es sich um einen Export aus dem Courseware-Plugin. Diese Archive sind nicht kompatibel mit dieser Courseware.'
-                        ));
-                        filesError = true;
-                    }
-                }
-            }
-            if (filesError) {
-                this.updateRequirements(slot, text, false);
-                slot.valid = false;
-                return;
-            } else {
-                this.updateRequirements(slot, text, true);
-                slot.valid = true;
-            }
-
-            let data = await this.zip.file('courseware.json').async('string');
-            let courseware = null;
-            let data_settings = null;
-            let settings = null;
-            let data_files = await this.zip.file('files.json').async('string');
-            let files = null;
-            let jsonErrors = false;
-
-            try {
-                courseware = JSON.parse(data);
-            } catch (error) {
-                jsonErrors = true;
-                this.archiveErrors.push(this.$gettext('Die Beschreibung der Courseware-Inhalte ist nicht valide.'));
-                this.archiveErrors.push(error);
-            }
-
-            if (this.zip.file('settings.json') !== null) {
-                data_settings = await this.zip.file('settings.json').async('string');
-                try {
-                    settings = JSON.parse(data_settings);
-                } catch (error) {
-                    jsonErrors = true;
-                    this.archiveErrors.push(this.$gettext('Die Beschreibung der Courseware-Einstellungen ist nicht valide.'));
-                    this.archiveErrors.push(error);
-                }
-            }
-
-            try {
-                files = JSON.parse(data_files);
-            } catch (error) {
-                jsonErrors = true;
-                this.archiveErrors.push(this.$gettext('Die Beschreibung der Dateien ist nicht valide.'));
-                this.archiveErrors.push(error);
-            }
-            if (jsonErrors) {
-                return;
-            }
-
-            this.loadedZipData = {
-                courseware: courseware,
-                files: files,
-                settings: settings
-            }
-
-            this.modifiedData.title = courseware.attributes.title;
-            this.modifiedData.color = courseware.attributes.payload.color ?? 'studip-blue';
-            this.modifiedData.description = courseware.attributes.payload.description ?? '';
-        },
-
-        async importCoursewareArchiv() {
-            if (this.loadedZipData === null) {
-                return false;
-            }
-
-            this.setImportFilesProgress(0);
-            this.setImportStructuresProgress(0);
-            this.setImportErrors([]);
-
-            this.importRunning = true;
-
-            const title = this.modifiedData.title !==  '' ? this.modifiedData.title : this.loadedTitle;
-            const description = this.modifiedData.description !==  '' ? this.modifiedData.description : this.loadedDescription;
-
-            const unit = {
-                    attributes: {
-                        title: title,
-                        payload: {
-                            description: description,
-                            color: this.modifiedData.color,
-                        }
-                    },
-                    relationships: {
-                        range: {
-                            data: {
-                                type: this.context.type,
-                                id: this.context.id
-                            }
-                        }
-                    }
-                };
-            await this.createCoursewareUnit(unit, { root: true });
-            const newElementId = this.lastCreateCoursewareUnit.relationships['structural-element'].data.id;
-            await this.loadStructuralElementById({ id: newElementId });
-
-            const newStructuralElement = this.structuralElementById({id: newElementId});
-
-            await this.importCourseware(this.loadedZipData.courseware, newStructuralElement.id, this.loadedZipData.files, 'migrate', this.loadedZipData.settings);
-            this.companionSuccess({ info: this.$gettext('Lernmaterial importiert.') });
-        },
-        updateRequirements(slot, text, valid) {
-            const index = this.requirements.findIndex(req =>  req.slot.id === slot.id && req.text === text);
-            if (valid) {
-                if (index !== -1) {
-                    this.requirements.splice(index, 1);
-                }
-            } else {
-                if (index === -1) {
-                   this.requirements.push({slot: slot, text: text});
-                }
-            }
-        }
-    }
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareShelfImportWidget.vue b/resources/vue/components/courseware/CoursewareShelfImportWidget.vue
deleted file mode 100644
index 2c946e2..0000000
--- a/resources/vue/components/courseware/CoursewareShelfImportWidget.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<template>
-    <sidebar-widget :title="$gettext('Import')">
-        <template #content>
-            <ul class="widget-list widget-links cw-import-widget">
-                <li class="cw-import-widget-archive">
-                    <button @click="setShowUnitImportDialog(true)">
-                        {{ $gettext('Lernmaterial importieren') }}
-                    </button>
-                </li>
-                <li class="cw-import-widget-copy">
-                    <button @click="setShowUnitCopyDialog(true)">
-                        {{ $gettext('Lernmaterial kopieren') }}
-                    </button>
-                </li>
-            </ul>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-shelf-import-widget',
-    components: {
-        SidebarWidget,
-    },
-    methods: {
-        ...mapActions({
-            setShowUnitCopyDialog: 'setShowUnitCopyDialog',
-            setShowUnitImportDialog: 'setShowUnitImportDialog',
-        }),
-    }
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue
deleted file mode 100644
index eab4c6a..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElement.vue
+++ /dev/null
@@ -1,1762 +0,0 @@
-<template>
-    <focus-trap v-model="consumModeTrap">
-        <div>
-            <div
-                :class="{ 'cw-structural-element-consumemode': consumeMode }"
-                class="cw-structural-element"
-                v-if="validContext"
-            >
-                <div class="cw-structural-element-content" v-if="structuralElement">
-                    <courseware-ribbon :canEdit="canEdit && canAddElements" :isContentBar="true" @blockAdded="updateContainerList">
-                        <template #buttons>
-                            <router-link v-if="prevElement" :to="'/structural_element/' + prevElement.id">
-                                <div class="cw-ribbon-button cw-ribbon-button-prev" :title="textRibbon.perv" />
-                            </router-link>
-                            <div v-else class="cw-ribbon-button cw-ribbon-button-prev-disabled" :title="$gettext('Keine vorherige Seite')"/>
-                            <router-link v-if="nextElement" :to="'/structural_element/' + nextElement.id">
-                                <div class="cw-ribbon-button cw-ribbon-button-next" :title="textRibbon.next" />
-                            </router-link>
-                            <div v-else class="cw-ribbon-button cw-ribbon-button-next-disabled" :title="$gettext('Keine nächste Seite')"/>
-                        </template>
-                        <template #breadcrumbList>
-                            <li
-                                v-for="ancestor in ancestors"
-                                :key="ancestor.id"
-                                :title="ancestor.attributes.title"
-                                class="cw-ribbon-breadcrumb-item"
-                            >
-                                <span>
-                                    <router-link :to="'/structural_element/' + ancestor.id">{{ ancestor.attributes.title || "–" }}</router-link>
-                                </span>
-                            </li>
-                            <li
-                                class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current"
-                                :title="structuralElement.attributes.title"
-                            >
-                                <span>{{ structuralElement.attributes.title || "–" }}</span>
-                                <span v-if="isTask">[ {{ solverName }} ]</span>
-                                <template v-if="!userIsTeacher && inCourse">
-                                    <studip-icon
-                                        v-if="complete"
-                                        shape="accept"
-                                        role="info"
-                                        :title="$gettext('Diese Seite wurde von Ihnen vollständig bearbeitet')"
-                                    />
-                                    <span
-                                        v-else
-                                        :title="$gettextInterpolate(
-                                                    $gettext('Fortschritt: %{progress} %'),
-                                                    {progress: elementProgress}
-                                                )"
-                                    >
-                                        ({{ elementProgress }} %)
-                                    </span>
-                                </template>
-                            </li>
-                        </template>
-                        <template #breadcrumbFallback>
-                            <li
-                                class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current"
-                                :title="structuralElement.attributes.title"
-                            >
-                                <span>{{ structuralElement.attributes.title }}</span>
-                            </li>
-                        </template>
-                        <template #menu>
-                            <studip-action-menu
-                                v-if="!consumeMode"
-                                :items="menuItems"
-                                class="cw-ribbon-action-menu"
-                                :context="structuralElement.attributes.title"
-                                @editCurrentElement="menuAction('editCurrentElement')"
-                                @addElement="menuAction('addElement')"
-                                @deleteCurrentElement="menuAction('deleteCurrentElement')"
-                                @showInfo="menuAction('showInfo')"
-                                @showExportOptions="menuAction('showExportOptions')"
-                                @oerCurrentElement="menuAction('oerCurrentElement')"
-                                @setBookmark="menuAction('setBookmark')"
-                                @sortContainers="menuAction('sortContainers')"
-                                @pdfExport="menuAction('pdfExport')"
-                                @showSuggest="menuAction('showSuggest')"
-                                @linkElement="menuAction('linkElement')"
-                                @removeLock="menuAction('removeLock')"
-                                @activateFullscreen="menuAction('activateFullscreen')"
-                            />
-                        </template>
-                    </courseware-ribbon>
-
-                    <div v-if="structuralElementLoaded && !isLink" class="cw-companion-box-wrapper">
-                        <courseware-companion-box
-                            v-if="!canVisit"
-                            mood="sad"
-                            :msgCompanion="$gettext('Diese Seite steht Ihnen leider nicht zur Verfügung.')"
-                        />
-                        <courseware-companion-box
-                            v-if="blockedByAnotherUser"
-                            :msgCompanion="$gettextInterpolate($gettext('Die Einstellungen dieser Seite werden im Moment von %{blockingUserName} bearbeitet'), {blockingUserName: blockingUserName})"
-                            mood="pointing"
-                        >
-                            <template #companionActions>
-                                <button v-if="userIsTeacher" class="button" @click="menuAction('removeLock')">
-                                    {{ textRemoveLock.title }}
-                                </button>
-                            </template>
-                        </courseware-companion-box>
-                        <courseware-empty-element-box
-                            v-if="showEmptyElementBox"
-                            :canEdit="canEdit"
-                            :noContainers="noContainers"
-                        />
-                        <courseware-welcome-screen v-if="noContainers && isRoot && canEdit" />
-                    </div>
-
-                    <div
-                        v-if="canVisit && !editView && !isLink"
-                        class="cw-container-wrapper"
-                        :class="{
-                            'cw-container-wrapper-consume': consumeMode,
-                            'cw-container-wrapper-discuss': discussView,
-                        }"
-                    >
-                        <courseware-structural-element-discussion
-                            v-if="!noContainers && discussView"
-                            :structuralElement="structuralElement"
-                            :canEdit="canEdit"
-                        />
-                        <component
-                            v-for="container in containers"
-                            :key="container.id"
-                            :is="containerComponent(container)"
-                            :container="container"
-                            :canEdit="canEdit"
-                            :canAddElements="canAddElements"
-                            :isTeacher="userIsTeacher"
-                            class="cw-container-item"
-                        />
-                    </div>
-                    <div
-                        v-if="isLink"
-                        class="cw-container-wrapper"
-                        :class="{
-                            'cw-container-wrapper-consume': consumeMode,
-                            'cw-container-wrapper-discuss': discussView,
-                        }"
-                    >
-                        <courseware-structural-element-discussion
-                            v-if="discussView"
-                            :structuralElement="structuralElement"
-                            :canEdit="canEdit"
-                        />
-                        <div v-if="editView" class="cw-companion-box-wrapper">
-                            <courseware-companion-box
-                                :msgCompanion="$gettextInterpolate($gettext('Dieser Inhalt ist aus den persönlichen Lernmaterialien von %{ ownerName } verlinkt und kann nur dort bearbeitet werden.'), { ownerName: ownerName })"
-                                mood="pointing"
-                            />
-                        </div>
-                        <component
-                            v-for="container in linkedContainers"
-                            :key="container.id"
-                            :is="containerComponent(container)"
-                            :container="container"
-                            :canEdit="false"
-                            :canAddElements="false"
-                            :isTeacher="userIsTeacher"
-                            class="cw-container-item"
-                        />
-                    </div>
-                    <div v-if="canVisit && canEdit && editView && !isLink" class="cw-container-wrapper cw-container-wrapper-edit">
-                        <template v-if="!processing">
-                            <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
-                            <span id="operation" class="assistive-text">
-                                {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
-                            </span>
-                            <draggable
-                                class="cw-structural-element-list"
-                                tag="ol"
-                                role="listbox"
-                                v-model="containerList"
-                                v-bind="dragOptions"
-                                handle=".cw-sortable-handle"
-                                @start="isDragging = true"
-                                @end="dropContainer"
-                            >
-                                <li
-                                    v-for="container in containerList"
-                                    :key="container.id"
-                                    class="cw-container-item-sortable"
-                                >
-                                    <span
-                                        :class="{ 'cw-sortable-handle-dragging': isDragging }"
-                                        class="cw-sortable-handle"
-                                        tabindex="0"
-                                        role="button"
-                                        aria-describedby="operation"
-                                        :ref="'sortableHandle' + container.id"
-                                        @keydown="keyHandler($event, container.id)"
-                                    ></span>
-                                    <component
-                                        :is="containerComponent(container)"
-                                        :container="container"
-                                        :canEdit="canEdit"
-                                        :canAddElements="canAddElements"
-                                        :isTeacher="userIsTeacher"
-                                        class="cw-container-item"
-                                        ref="containers"
-                                        :class="{ 'cw-container-item-selected': keyboardSelected === container.id}"
-                                    />
-                                </li>
-                            </draggable>
-                        </template>
-                        <studip-progress-indicator v-if="processing" :description="$gettext('Vorgang wird bearbeitet...')" />
-                    </div>
-                </div>
-
-                <studip-dialog
-                    v-if="showEditDialog"
-                    :title="textEdit.title"
-                    :confirmText="textEdit.confirm"
-                    confirmClass="accept"
-                    :closeText="textEdit.close"
-                    closeClass="cancel"
-                    height="500"
-                    :width="inContent ? '720' : '500'"
-                    class="studip-dialog-with-tab"
-                    @close="closeEditDialog"
-                    @confirm="storeCurrentElement"
-                >
-                    <template v-slot:dialogContent>
-                        <courseware-tabs class="cw-tab-in-dialog">
-                            <courseware-tab :name="textEdit.basic" :selected="true" :index="0">
-                                <form class="default" @submit.prevent="">
-                                    <label>
-                                        <translate>Titel</translate>
-                                        <input type="text" v-model="currentElement.attributes.title" />
-                                    </label>
-                                    <label>
-                                        <translate>Beschreibung</translate>
-                                        <textarea
-                                            v-model="currentElement.attributes.payload.description"
-                                            class="cw-structural-element-description"
-                                        />
-                                    </label>
-                                </form>
-                            </courseware-tab>
-                            <courseware-tab :name="textEdit.meta" :index="1">
-                                <form class="default" @submit.prevent="">
-                                    <label>
-                                        <translate>Farbe</translate>
-                                        <studip-select
-                                            v-model="currentElement.attributes.payload.color"
-                                            :options="colors"
-                                            :reduce="(color) => color.class"
-                                            label="class"
-                                            class="cw-vs-select"
-                                        >
-                                            <template #open-indicator="selectAttributes">
-                                                <span v-bind="selectAttributes"
-                                                    ><studip-icon shape="arr_1down" :size="10"
-                                                /></span>
-                                            </template>
-                                            <template #no-options>
-                                                <translate>Es steht keine Auswahl zur Verfügung</translate>.
-                                            </template>
-                                            <template #selected-option="{ name, hex }">
-                                                <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                                                ><span>{{ name }}</span>
-                                            </template>
-                                            <template #option="{ name, hex }">
-                                                <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                                                ><span>{{ name }}</span>
-                                            </template>
-                                        </studip-select>
-                                    </label>
-                                    <label>
-                                        <translate>Art des Lernmaterials</translate>
-                                        <select v-model="currentElement.attributes.purpose">
-                                            <option value="content"><translate>Inhalt</translate></option>
-                                            <option v-if="!inCourse"  value="template"><translate>Aufgabenvorlage</translate></option>
-                                            <option value="oer"><translate>OER-Material</translate></option>
-                                            <option value="portfolio"><translate>ePortfolio</translate></option>
-                                            <option value="draft"><translate>Entwurf</translate></option>
-                                            <option value="other"><translate>Sonstiges</translate></option>
-                                        </select>
-                                    </label>
-                                    <label>
-                                        <translate>Lizenztyp</translate>
-                                        <select v-model="currentElement.attributes.payload.license_type">
-                                            <option v-for="license in licenses" :key="license.id" :value="license.id">
-                                                {{ license.name }}
-                                            </option>
-                                        </select>
-                                    </label>
-                                    <label>
-                                        <translate>Geschätzter zeitlicher Aufwand</translate>
-                                        <input type="text" v-model="currentElement.attributes.payload.required_time" />
-                                    </label>
-                                    <label>
-                                        <translate>Niveau</translate><br />
-                                        <translate>von</translate>
-                                        <select v-model="currentElement.attributes.payload.difficulty_start">
-                                            <option
-                                                v-for="difficulty_start in 12"
-                                                :key="difficulty_start"
-                                                :value="difficulty_start"
-                                            >
-                                                {{ difficulty_start }}
-                                            </option>
-                                        </select>
-                                        <translate>bis</translate>
-                                        <select v-model="currentElement.attributes.payload.difficulty_end">
-                                            <option
-                                                v-for="difficulty_end in 12"
-                                                :key="difficulty_end"
-                                                :value="difficulty_end"
-                                            >
-                                                {{ difficulty_end }}
-                                            </option>
-                                        </select>
-                                    </label>
-                                </form>
-                            </courseware-tab>
-                            <courseware-tab :name="textEdit.image" :index="2">
-                                <form class="default" @submit.prevent="">
-                                    <template v-if="hasImage">
-                                        <img
-                                            :src="image"
-                                            class="cw-structural-element-image-preview"
-                                            :alt="$gettext('Vorschaubild')"
-                                            />
-                                        <label>
-                                            <button class="button" @click="deleteImage" v-translate>Bild löschen</button>
-                                        </label>
-                                    </template>
-
-                                    <div v-else class="cw-structural-element-image-preview-placeholder"></div>
-
-                                    <div v-if="uploadFileError" class="messagebox messagebox_error">
-                                        {{ uploadFileError }}
-                                    </div>
-
-                                    <div v-show="!hasImage">
-                                        <label>
-                                            {{ $gettext('Bild hochladen') }}
-                                            <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
-                                        </label>
-                                        {{ $gettext('oder') }}
-                                        <br>
-                                        <button class="button" type="button" @click="showStockImageSelector = true">
-                                            {{ $gettext('Aus dem Bilderpool auswählen') }}
-                                        </button>
-                                        <StockImageSelector v-if="showStockImageSelector" @close="showStockImageSelector = false" @select="onSelectStockImage" />
-                                    </div>
-                                </form>
-                            </courseware-tab>
-                            <courseware-tab v-if="(inCourse && !isTask) || inContent" :name="textEdit.approval" :index="3">
-                                <courseware-structural-element-permissions
-                                    v-if="inCourse"
-                                    :element="currentElement"
-                                    @updateReadApproval="updateReadApproval"
-                                    @updateWriteApproval="updateWriteApproval"
-                                />
-                                <courseware-content-permissions
-                                    v-if="inContent"
-                                    :element="currentElement"
-                                    @updateReadApproval="updateReadApproval"
-                                    @updateWriteApproval="updateWriteApproval"
-                                />
-                            </courseware-tab>
-                            <courseware-tab v-if="inCourse && !isTask" :name="textEdit.visible" :index="4">
-                                <form class="default" @submit.prevent="">
-                                    <label>
-                                        <translate>Sichtbar ab</translate>
-                                        <input type="date" v-model="currentElement.attributes['release-date']" />
-                                    </label>
-                                    <label>
-                                        <translate>Unsichtbar ab</translate>
-                                        <input type="date" v-model="currentElement.attributes['withdraw-date']" />
-                                    </label>
-                                </form>
-                            </courseware-tab>
-                        </courseware-tabs>
-                    </template>
-                </studip-dialog>
-                <courseware-structural-element-dialog-add
-                    v-if="showAddDialog"
-                    :structuralElement="structuralElement"
-                    :isRoot="isRoot"
-                    :canEditParent="canEditParent"
-                />
-                <studip-dialog
-                    v-if="showInfoDialog"
-                    :title="textInfo.title"
-                    :closeText="textInfo.close"
-                    closeClass="cancel"
-                    @close="showElementInfoDialog(false)"
-                >
-                    <template v-slot:dialogContent>
-                        <table class="cw-structural-element-info">
-                            <tr>
-                                <td><translate>Titel</translate>:</td>
-                                <td>{{ structuralElement.attributes.title }}</td>
-                            </tr>
-                            <tr>
-                                <td><translate>Beschreibung</translate>:</td>
-                                <td>{{ structuralElement.attributes.payload.description }}</td>
-                            </tr>
-                            <tr>
-                                <td><translate>Seite wurde erstellt von</translate>:</td>
-                                <td>{{ ownerName }}</td>
-                            </tr>
-                            <tr>
-                                <td><translate>Seite wurde erstellt am</translate>:</td>
-                                <td><iso-date :date="structuralElement.attributes.mkdate" /></td>
-                            </tr>
-                            <tr>
-                                <td><translate>Zuletzt bearbeitet von</translate>:</td>
-                                <td>{{ editorName }}</td>
-                            </tr>
-                            <tr>
-                                <td><translate>Zuletzt bearbeitet am</translate>:</td>
-                                <td><iso-date :date="structuralElement.attributes.chdate" /></td>
-                            </tr>
-                        </table>
-                    </template>
-                </studip-dialog>
-
-                <studip-dialog
-                    v-if="showExportDialog"
-                    :title="textExport.title"
-                    :confirmText="textExport.confirm"
-                    confirmClass="accept"
-                    :closeText="textExport.close"
-                    closeClass="cancel"
-                    height="350"
-                    @close="showElementExportDialog(false)"
-                    @confirm="exportCurrentElement"
-                >
-                    <template v-slot:dialogContent>
-                        <div v-show="!exportRunning">
-                            <span v-translate>Hiermit exportieren Sie die Seite "%{ currentElement.attributes.title }" als ZIP-Datei.</span>
-                            <div class="cw-element-export">
-                                <label>
-                                    <input type="checkbox" v-model="exportChildren" />
-                                    <translate>Unterseiten exportieren</translate>
-                                </label>
-                            </div>
-                        </div>
-
-                        <courseware-companion-box
-                            v-show="exportRunning"
-                            :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')"
-                            mood="pointing"
-                        />
-                        <div v-show="exportRunning" class="cw-import-zip">
-                            <header>{{ exportState }}:</header>
-                            <div class="progress-bar-wrapper">
-                                <div
-                                    class="progress-bar"
-                                    role="progressbar"
-                                    :style="{ width: exportProgress + '%' }"
-                                    :aria-valuenow="exportProgress"
-                                    aria-valuemin="0"
-                                    aria-valuemax="100"
-                                >
-                                    {{ exportProgress }}%
-                                </div>
-                            </div>
-                        </div>
-                    </template>
-                </studip-dialog>
-
-                <studip-dialog
-                    v-if="showPdfExportDialog"
-                    :title="textExport.title"
-                    :confirmText="textExport.confirm"
-                    confirmClass="accept"
-                    :closeText="textExport.close"
-                    closeClass="cancel"
-                    height="350"
-                    @close="showElementPdfExportDialog(false)"
-                    @confirm="pdfExportCurrentElement"
-                >
-                    <template v-slot:dialogContent>
-                        <span v-translate>Hiermit exportieren Sie die Seite "%{ currentElement.attributes.title }" als PDF-Datei.</span>
-                            <div class="cw-element-export">
-                                <label>
-                                    <input type="checkbox" v-model="pdfExportChildren" />
-                                    <translate>Unterseiten exportieren</translate>
-                                </label>
-                            </div>
-                    </template>
-                </studip-dialog>
-
-                <studip-dialog
-                    v-if="showOerDialog"
-                    height="600"
-                    width="600"
-                    :title="textOer.title"
-                    :confirmText="textOer.confirm"
-                    confirmClass="accept"
-                    :closeText="textOer.close"
-                    closeClass="cancel"
-                    @close="showElementOerDialog(false)"
-                    @confirm="publishCurrentElement"
-                >
-                    <template v-slot:dialogContent>
-                        <form v-show="!oerExportRunning" class="default" @submit.prevent="">
-                            <fieldset>
-                                <legend><translate>Grunddaten</translate></legend>
-                                <label>
-                                    <p><translate>Vorschaubild</translate>:</p>
-                                    <img
-                                        v-if="currentElement.relationships.image.data"
-                                        :src="currentElement.relationships.image.meta['download-url']"
-                                        width="400"
-                                    />
-                                </label>
-                                <label>
-                                    <p><translate>Beschreibung</translate>:</p>
-                                    <p>{{ currentElement.attributes.payload.description }}</p>
-                                </label>
-                                <label>
-                                    <translate>Niveau</translate>:
-                                    <p>
-                                        {{ currentElement.attributes.payload.difficulty_start }} -
-                                        {{ currentElement.attributes.payload.difficulty_end }}
-                                    </p>
-                                </label>
-                                <label>
-                                    <translate>Lizenztyp</translate>:
-                                    <p>{{ currentLicenseName }}</p>
-                                </label>
-                                <label>
-                                    <translate>Sie können diese Daten unter "Seiteneinstellungen" verändern.</translate>
-                                </label>
-                            </fieldset>
-                            <fieldset>
-                                <legend><translate>Einstellungen</translate></legend>
-                                <label>
-                                    <translate>Unterseiten veröffentlichen</translate>
-                                    <input type="checkbox" v-model="oerChildren" />
-                                </label>
-                            </fieldset>
-                        </form>
-                        <courseware-companion-box
-                            v-show="oerExportRunning"
-                            :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')"
-                            mood="pointing"
-                        />
-                    </template>
-                </studip-dialog>
-                <studip-dialog
-                    v-if="showSuggestOerDialog"
-                    height="600"
-                    width="600"
-                    :title="textSuggestOer.title"
-                    :confirmText="textSuggestOer.confirm"
-                    confirmClass="accept"
-                    :closeText="textSuggestOer.close"
-                    closeClass="cancel"
-                    @close="updateShowSuggestOerDialog(false)"
-                    @confirm="sendOerSuggestion"
-                >
-                    <template v-slot:dialogContent>
-                        <p v-translate>Das folgende Courseware-Material wird %{ ownerName }
-                            zur Veröffentlichung im OER Campus vorgeschlagen:</p>
-                        <table class="cw-structural-element-info">
-                            <tr>
-                                <td><translate>Titel</translate>:</td>
-                                <td>{{ structuralElement.attributes.title }}</td>
-                            </tr>
-                            <tr>
-                                <td><translate>Beschreibung</translate>:</td>
-                                <td>{{ structuralElement.attributes.payload.description }}</td>
-                            </tr>
-                        </table>
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                <translate>Ihr Vorschlag wird anonym versendet. Falls gewünscht, können Sie
-                                    zusätzlich eine Nachricht verfassen:</translate>
-                                <textarea
-                                    v-model="additionalText"
-                                    class="cw-structural-element-description"
-                                />
-                            </label>
-                        </form>
-                    </template>
-                </studip-dialog>
-                <studip-dialog
-                    v-if="showDeleteDialog"
-                    :title="textDelete.title"
-                    :question="textDelete.alert"
-                    height="200"
-                    @confirm="deleteCurrentElement"
-                    @close="closeDeleteDialog"
-                ></studip-dialog>
-                <studip-dialog
-                    v-if="showPublicLinkDialog && inContent"
-                    :title="$gettext('Öffentlichen Link für Seite erzeugen')"
-                    :confirmText="$gettext('Erstellen')"
-                    confirmClass="accept"
-                    :closeText="$gettext('Abbrechen')"
-                    closeClass="cancel"
-                    class="cw-structural-element-dialog"
-                    @close="closePublicLinkDialog"
-                    @confirm="createElementPublicLink"
-                >
-                    <template v-slot:dialogContent>
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                <translate>Passwort</translate>
-                                <input type="password" v-model="publicLink.password" />
-                            </label>
-                            <label>
-                                <translate>Ablaufdatum</translate>
-                                <input v-model="publicLink['expire-date']" type="date" class="size-l" />
-                            </label>
-                        </form>
-                    </template>
-                </studip-dialog>
-                <studip-dialog
-                    v-if="showRemoveLockDialog"
-                    :title="textRemoveLock.title"
-                    :question="textRemoveLock.alert"
-                    height="200"
-                    width="450"
-                    @confirm="executeRemoveLock"
-                    @close="showElementRemoveLockDialog(false)"
-                ></studip-dialog>
-
-                <courseware-structural-element-dialog-import v-if="showImportDialog"/>
-                <courseware-structural-element-dialog-copy v-if="showCopyDialog" />
-                <courseware-structural-element-dialog-link v-if="showLinkDialog"/>
-            </div>
-            <div v-else>
-                <courseware-companion-box
-                    v-if="currentElement !== ''"
-                    :msgCompanion="textCompanionWrongContext"
-                    mood="sad"
-                >
-                    <template v-slot:companionActions >
-                        <a class="button" :href="unitRootUrl">{{ $gettext('Lernmaterial neu laden') }}</a>
-                        <a class="button" :href="shelfURL">{{ $gettext('Zurück zur Lernmaterialübersicht') }}</a>
-                    </template>
-                </courseware-companion-box>
-            </div>
-        </div>
-    </focus-trap>
-</template>
-
-<script>
-import ContainerComponents from './container-components.js';
-import CoursewarePluginComponents from './plugin-components.js';
-import CoursewareStructuralElementDialogAdd from './CoursewareStructuralElementDialogAdd.vue';
-import CoursewareStructuralElementDialogCopy from './CoursewareStructuralElementDialogCopy.vue';
-import CoursewareStructuralElementDialogImport from './CoursewareStructuralElementDialogImport.vue';
-import CoursewareStructuralElementDialogLink from './CoursewareStructuralElementDialogLink.vue';
-import CoursewareStructuralElementDiscussion from './CoursewareStructuralElementDiscussion.vue';
-import CoursewareStructuralElementPermissions from './CoursewareStructuralElementPermissions.vue';
-import CoursewareContentPermissions from './CoursewareContentPermissions.vue';
-import CoursewareAccordionContainer from './CoursewareAccordionContainer.vue';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareWelcomeScreen from './CoursewareWelcomeScreen.vue';
-import CoursewareEmptyElementBox from './CoursewareEmptyElementBox.vue';
-import CoursewareListContainer from './CoursewareListContainer.vue';
-import CoursewareTabsContainer from './CoursewareTabsContainer.vue';
-import CoursewareRibbon from './CoursewareRibbon.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import CoursewareExport from '@/vue/mixins/courseware/export.js';
-import CoursewareOerMessage from '@/vue/mixins/courseware/oermessage.js';
-import colorMixin from '@/vue/mixins/courseware/colors.js';
-import wizardMixin from '@/vue/mixins/courseware/wizard.js';
-import CoursewareDateInput from './CoursewareDateInput.vue';
-import { FocusTrap } from 'focus-trap-vue';
-import IsoDate from './IsoDate.vue';
-import StockImageSelector from '../stock-images/SelectorDialog.vue';
-import StudipDialog from '../StudipDialog.vue';
-import draggable from 'vuedraggable';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-structural-element',
-    components: {
-        CoursewareStructuralElementDialogAdd,
-        CoursewareStructuralElementDialogCopy,
-        CoursewareStructuralElementDialogImport,
-        CoursewareStructuralElementDialogLink,
-        CoursewareStructuralElementDiscussion,
-        CoursewareStructuralElementPermissions,
-        CoursewareContentPermissions,
-        CoursewareRibbon,
-        CoursewareListContainer,
-        CoursewareAccordionContainer,
-        CoursewareTabsContainer,
-        CoursewareCompanionBox,
-        CoursewareWelcomeScreen,
-        CoursewareEmptyElementBox,
-        CoursewareTabs,
-        CoursewareTab,
-        CoursewareDateInput,
-        FocusTrap,
-        IsoDate,
-        StockImageSelector,
-        StudipDialog,
-        draggable,
-    },
-    props: ['canVisit', 'orderedStructuralElements', 'structuralElement'],
-
-    mixins: [CoursewareExport, CoursewareOerMessage, colorMixin, wizardMixin],
-
-    data() {
-        return {
-            currentElement: '',
-            uploadFileError: '',
-            textCompanionWrongContext: this.$gettext('Die angeforderte Seite ist nicht Teil dieser Courseware.'),
-            textEdit: {
-                title: this.$gettext('Seiteneinstellungen'),
-                confirm: this.$gettext('Speichern'),
-                close: this.$gettext('Schließen'),
-                basic: this.$gettext('Grunddaten'),
-                image: this.$gettext('Bild'),
-                meta: this.$gettext('Metadaten'),
-                approval: this.$gettext('Rechte'),
-                visible: this.$gettext('Sichtbarkeit'),
-            },
-            textInfo: {
-                title: this.$gettext('Informationen zur Seite'),
-                close: this.$gettext('Schließen'),
-            },
-            textExport: {
-                title: this.$gettext('Seite exportieren'),
-                confirm: this.$gettext('Exportieren'),
-                close: this.$gettext('Schließen'),
-            },
-            textAdd: {
-                title: this.$gettext('Seite hinzufügen'),
-                confirm: this.$gettext('Erstellen'),
-                close: this.$gettext('Schließen'),
-            },
-            textRibbon: {
-                perv: this.$gettext('zurück'),
-                next: this.$gettext('weiter'),
-            },
-            textRemoveLock: {
-                title: this.$gettext('Sperre aufheben'),
-                alert: this.$gettext('Möchten Sie die Sperre der Seite wirklich aufheben?'),
-            },
-            exportRunning: false,
-            exportChildren: false,
-            oerExportRunning: false,
-            oerChildren: true,
-            pdfExportChildren: false,
-            containerList: [],
-            isDragging: false,
-            dragOptions: {
-                animation: 0,
-                group: 'description',
-                disabled: false,
-                ghostClass: 'container-ghost',
-            },
-            errorEmptyChapterName: false,
-            consumModeTrap: false,
-            additionalText: '',
-
-            publicLink: {
-                passsword: '',
-                'expire-date': ''
-            },
-            deletingPreviewImage: false,
-            processing: false,
-            keyboardSelected: null,
-            assistiveLive: '',
-            uploadImageURL: null,
-            showStockImageSelector: false,
-            selectedStockImage: null,
-        };
-    },
-
-    computed: {
-        ...mapGetters({
-            courseware: 'courseware',
-            context: 'context',
-            consumeMode: 'consumeMode',
-            containerById: 'courseware-containers/byId',
-            relatedContainers: 'courseware-containers/related',
-            relatedStructuralElements: 'courseware-structural-elements/related',
-            relatedTaskGroups: 'courseware-task-groups/related',
-            relatedUsers: 'users/related',
-            structuralElementById: 'courseware-structural-elements/byId',
-            userIsTeacher: 'userIsTeacher',
-            pluginManager: 'pluginManager',
-            showEditDialog: 'showStructuralElementEditDialog',
-            showAddDialog: 'showStructuralElementAddDialog',
-            showImportDialog: 'showStructuralElementImportDialog',
-            showCopyDialog: 'showStructuralElementCopyDialog',
-            showLinkDialog: 'showStructuralElementLinkDialog',
-            showExportDialog: 'showStructuralElementExportDialog',
-            showPdfExportDialog: 'showStructuralElementPdfExportDialog',
-            showInfoDialog: 'showStructuralElementInfoDialog',
-            showDeleteDialog: 'showStructuralElementDeleteDialog',
-            showOerDialog: 'showStructuralElementOerDialog',
-            showSuggestOerDialog: 'showSuggestOerDialog',
-            showPublicLinkDialog: 'showStructuralElementPublicLinkDialog',
-            showRemoveLockDialog: 'showStructuralElementRemoveLockDialog',
-            oerCampusEnabled: 'oerCampusEnabled',
-            oerEnableSuggestions: 'oerEnableSuggestions',
-            licenses: 'licenses',
-            exportState: 'exportState',
-            exportProgress: 'exportProgress',
-            userId: 'userId',
-            viewMode: 'viewMode',
-            taskById: 'courseware-tasks/byId',
-            userById: 'users/byId',
-            lastCreatedElement: 'courseware-structural-elements/lastCreated',
-            groupById: 'status-groups/byId',
-
-            blocked: 'currentElementBlocked',
-            blockerId: 'currentElementBlockerId',
-            blockedByThisUser: 'currentElementBlockedByThisUser',
-            blockedByAnotherUser: 'currentElementBlockedByAnotherUser',
-            isLink: 'currentElementisLink',
-
-            templates: 'courseware-templates/all',
-            progressData: 'progresses',
-        }),
-
-        currentId() {
-            return this.structuralElement?.id;
-        },
-
-        textOer() {
-            return {
-                title: this.$gettext('Lerninhalte auf dem OER Campus veröffentlichen'),
-                confirm: this.$gettext('Veröffentlichen'),
-                close: this.$gettext('Abbrechen'),
-            };
-        },
-
-        textSuggestOer() {
-            return {
-                title: this.$gettext('Lerninhalt für den OER Campus vorschlagen'),
-                confirm: this.$gettext('Lerninhalt vorschlagen'),
-                close: this.$gettext('Abbrechen'),
-            };
-        },
-
-        inCourse() {
-            return this.context.type === 'courses';
-        },
-
-        inContent() {
-            // The rights tab in contents will be only visible to the owner.
-            return this.context.type === 'users' && this.userId === this.currentElement.relationships.user.data.id;
-        },
-
-        textDelete() {
-            let textDelete = {};
-            textDelete.title = this.$gettext('Seite unwiderruflich löschen');
-            textDelete.alert = this.$gettext('Möchten Sie die Seite wirklich löschen?');
-            if (this.structuralElementLoaded) {
-                textDelete.alert =
-                    this.$gettextInterpolate(
-                        this.$gettext('Möchten Sie die Seite %{ pageTitle } und alle ihre Unterseiten wirklich löschen?'),
-                        {pageTitle: this.structuralElement.attributes.title}
-                    );
-            }
-
-            return textDelete;
-        },
-
-        validContext() {
-            if (this.context.type === 'sharedusers') {
-                if (this.context.id === this.courseware.relationships.root.data.id) {
-                    return true;
-                }
-            }
-
-            if (this.context.type === 'public') {
-                return true;
-            }
-
-            if (this.context.unit !== this.currentElement.relationships?.unit?.data?.id) {
-                return false;
-            }
-
-            if (this.context.type === 'courses' && this.currentElement.relationships) {
-                if (
-                    this.currentElement.relationships.course &&
-                    this.context.id === this.currentElement.relationships.course.data.id
-                ) {
-                    return true;
-                }
-            }
-
-            if (this.context.type === 'users' && this.currentElement.relationships) {
-                if (
-                    this.currentElement.relationships.user &&
-                    this.context.id === this.currentElement.relationships.user.data.id
-                ) {
-                    return true;
-                }
-            }
-            
-
-            return false;
-        },
-
-        image() {
-            if (this.selectedStockImage) {
-                return this.selectedStockImage.attributes['download-urls'].small
-            }
-            if (this.uploadImageURL) {
-                return this.uploadImageURL;
-            }
-            return this.structuralElement.relationships?.image?.meta?.['download-url'] ?? null;
-        },
-
-        imageType() {
-            return this.structuralElement.relationships?.image?.data?.type ?? null;
-        },
-
-        hasImage() {
-            return (this.image || this.selectedStockImage ) && this.deletingPreviewImage === false;
-        },
-
-        structuralElementLoaded() {
-            return this.structuralElement !== null && this.structuralElement !== {};
-        },
-
-        ancestors() {
-            if (!this.structuralElement) {
-                return [];
-            }
-
-            const finder = (parent) => {
-                const parentId = parent.relationships?.parent?.data?.id;
-                if (!parentId) {
-                    return null;
-                }
-                const element = this.structuralElementById({ id: parentId });
-                if (!element) {
-                    console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`);
-                }
-
-                return element;
-            };
-
-            const visitAncestors = function* (node) {
-                const parent = finder(node);
-                if (parent) {
-                    yield parent;
-                    yield* visitAncestors(parent);
-                }
-            };
-
-            return [...visitAncestors(this.structuralElement)].reverse();
-        },
-        prevElement() {
-            const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id);
-            if (currentIndex <= 0) {
-                return null;
-            }
-            const previousId = this.orderedStructuralElements[currentIndex - 1];
-            const previous = this.structuralElementById({ id: previousId });
-
-            return previous;
-        },
-        nextElement() {
-            const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id);
-            const lastIndex = this.orderedStructuralElements.length - 1;
-            if (currentIndex === -1 || currentIndex === lastIndex) {
-                return null;
-            }
-            const nextId = this.orderedStructuralElements[currentIndex + 1];
-            const next = this.structuralElementById({ id: nextId });
-
-            return next;
-        },
-        empty() {
-            if (this.containers === null) {
-                return true;
-            } else {
-                return !this.containers.some((container) => container.relationships.blocks.data.length > 0);
-            }
-        },
-        containers() {
-            if (!this.structuralElement) {
-                return [];
-            }
-
-            return (
-                this.relatedContainers({
-                    parent: this.structuralElement,
-                    relationship: 'containers',
-                }) ?? []
-            );
-        },
-        noContainers() {
-            if (this.containers === null) {
-                return true;
-            } else {
-                return this.containers.length === 0;
-            }
-        },
-
-        canEdit() {
-            if (!this.structuralElement) {
-                return false;
-            }
-            return this.structuralElement.attributes['can-edit'];
-        },
-
-        canEditParent() {
-            if (this.isRoot) {
-                return false;
-            }
-            const parentId = this.structuralElement.relationships.parent.data.id;
-            const parent = this.structuralElementById({ id: parentId });
-
-            return parent.attributes['can-edit'];
-        },
-
-        isRoot() {
-            return this.structuralElement.relationships.parent.data === null;
-        },
-
-        editor() {
-            const editor = this.relatedUsers({
-                parent: this.structuralElement,
-                relationship: 'editor',
-            });
-
-            return editor ?? null;
-        },
-
-        editorName() {
-            return this.editor?.attributes['formatted-name'] ?? '?';
-        },
-
-        menuItems() {
-            let menu = [
-                { id: 4, label: this.$gettext('Informationen anzeigen'), icon: 'info', emit: 'showInfo' },
-                { id: 5, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' },
-            ];
-
-            if (this.oerEnableSuggestions) {
-                menu.push(
-                    { id: 6, label: this.$gettext('Seite für OER Campus vorschlagen'), icon: 'oer-campus',
-                        emit: 'showSuggest' }
-                );
-            }
-
-            if (!document.documentElement.classList.contains('responsive-display')) {
-                menu.push(
-                    { id: 7, label: this.$gettext('Als Vollbild anzeigen'), icon: 'screen-full',
-                        emit: 'activateFullscreen'},
-                );
-            }
-
-            if (this.canEdit) {
-                if (!this.blockedByAnotherUser) {
-                    menu.push({
-                        id: 1,
-                        label: this.$gettext('Seiteneinstellungen'),
-                        icon: 'settings',
-                        emit: 'editCurrentElement',
-                    });
-                }
-                if (this.blockedByAnotherUser && this.userIsTeacher) {
-                    menu.push({
-                        id: 1,
-                        label: this.textRemoveLock.title,
-                        icon: 'lock-unlocked',
-                        emit: 'removeLock',
-                    });
-                }
-                menu.push({ id: 3, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' });
-            }
-            if (this.context.type === 'users') {
-                menu.push({ id: 8, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' });
-            }
-            if (!this.isRoot && this.canEdit && !this.isTask && !this.blocked) {
-                menu.push({
-                    id: 8,
-                    label: this.$gettext('Seite löschen'),
-                    icon: 'trash',
-                    emit: 'deleteCurrentElement',
-                });
-            }
-            menu.sort((a, b) => a.id - b.id);
-
-            return menu;
-        },
-        colors() {
-            return this.mixinColors.filter(color => color.darkmode);
-        },
-        currentLicenseName() {
-            for (let i = 0; i < this.licenses.length; i++) {
-                if (this.licenses[i]['id'] == this.currentElement.attributes.payload.license_type) {
-                    return this.licenses[i]['name'];
-                }
-            }
-
-            return '';
-        },
-        blockingUser() {
-            if (this.blockedByAnotherUser) {
-                return this.userById({id: this.blockerId});
-            }
-
-            return null;
-        },
-        blockingUserName() {
-            return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : '';
-        },
-        discussView() {
-            return this.viewMode === 'discuss';
-        },
-        editView() {
-            return this.viewMode === 'edit';
-        },
-        pdfExportURL() {
-            if (this.context.type === 'users') {
-                return STUDIP.URLHelper.getURL(
-                    'dispatch.php/contents/courseware/pdf_export/' + this.structuralElement.id
-                );
-            }
-            if (this.context.type === 'courses') {
-                return STUDIP.URLHelper.getURL(
-                    'dispatch.php/course/courseware/pdf_export/' + this.structuralElement.id
-                );
-            }
-
-            return '';
-        },
-        isTask() {
-            return this.structuralElement?.relationships.task.data !== null;
-        },
-        task() {
-            if (!this.isTask) {
-                return null;
-            }
-
-            return this.taskById({ id: this.structuralElement.relationships.task.data.id });
-        },
-        solver() {
-            if (this.task) {
-                const solver = this.task.relationships.solver.data;
-                if (solver.type === 'users') {
-                    return this.userById({ id: solver.id });
-                }
-                if (solver.type === 'status-groups') {
-                    return this.groupById({ id: solver.id });
-                }
-            }
-
-            return null;
-        },
-        solverName() {
-            if (this.solver) {
-                if (this.solver.type === 'users') {
-                    return this.solver.attributes['formatted-name'];
-                }
-                if (this.solver.type === 'status-groups') {
-                    return this.solver.attributes.name;
-                }
-            }
-
-            return '';
-        },
-        canAddElements() {
-            if (!this.isTask) {
-                return true;
-            }
-
-            // still loading
-            if (!this.task) {
-                return false;
-            }
-
-            const taskGroup = this.relatedTaskGroups({ parent: this.task, relationship: 'task-group' });
-
-            return taskGroup?.attributes['solver-may-add-blocks'];
-        },
-        showEmptyElementBox() {
-            if (!this.empty) {
-                return false;
-            }
-
-            return (
-                (!this.isRoot && this.canEdit) || !this.canEdit || (!this.noContainers && this.isRoot && this.canEdit)
-            );
-        },
-
-        linkedElement() {
-            if (this.isLink) {
-                return this.structuralElementById({ id: this.structuralElement.attributes['target-id']});
-            }
-
-            return null;
-        },
-
-        linkedContainers() {
-            let containers = [];
-            let relatedContainers = this.linkedElement?.relationships?.containers?.data;
-
-            if (relatedContainers) {
-                for (const container of relatedContainers) {
-                    containers.push(this.containerById({ id: container.id}));
-                }
-            }
-
-            return containers;
-        },
-
-        owner() {
-            const owner = this.relatedUsers({
-                parent: this.structuralElement,
-                relationship: 'owner',
-            });
-            return owner ?? null;
-        },
-
-        ownerName() {
-            return this.owner?.attributes['formatted-name'] ?? '?';
-        },
-        complete() {
-            return this.elementProgress === 100;
-        },
-        elementProgress() {
-            if (this.structuralElementLoaded) {
-                return this.progressData?.[this.structuralElement.id].progress.self;
-            }
-
-            return 0;
-        },
-        progressTitle() {
-            return '';
-        },
-        shelfURL() {
-            return STUDIP.URLHelper.getURL(
-                'dispatch.php/course/courseware/',
-                {cid: this.context.id}
-            );
-        },
-        unitRootUrl() {
-            return STUDIP.URLHelper.getURL(
-                'dispatch.php/course/courseware/courseware/' + this.context.unit,
-                {cid: this.context.id}
-            );
-        }
-    },
-
-    methods: {
-        ...mapActions({
-            updateStructuralElement: 'updateStructuralElement',
-            deleteStructuralElement: 'deleteStructuralElement',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-            addBookmark: 'addBookmark',
-            companionInfo: 'companionInfo',
-            companionWarning: 'companionWarning',
-            companionError: 'companionError',
-            uploadImageForStructuralElement: 'uploadImageForStructuralElement',
-            deleteImageForStructuralElement: 'deleteImageForStructuralElement',
-            companionSuccess: 'companionSuccess',
-            setStockImageForStructuralElement: 'setStockImageForStructuralElement',
-            showElementEditDialog: 'showElementEditDialog',
-            showElementAddDialog: 'showElementAddDialog',
-            showElementExportDialog: 'showElementExportDialog',
-            showElementPdfExportDialog: 'showElementPdfExportDialog',
-            showElementInfoDialog: 'showElementInfoDialog',
-            showElementDeleteDialog: 'showElementDeleteDialog',
-            showElementOerDialog: 'showElementOerDialog',
-            showElementPublicLinkDialog: 'showElementPublicLinkDialog',
-            showElementRemoveLockDialog: 'showElementRemoveLockDialog',
-            updateShowSuggestOerDialog: 'updateShowSuggestOerDialog',
-            updateContainer: 'updateContainer',
-            sortContainersInStructualElements: 'sortContainersInStructualElements',
-            loadTask: 'loadTask',
-            loadStructuralElement: 'loadStructuralElement',
-            createLink: 'createLink',
-            setCurrentElementId: 'coursewareCurrentElement',
-            loadProgresses: 'loadProgresses'
-        }),
-
-        initCurrent() {
-            this.currentElement = _.cloneDeep(this.structuralElement);
-            this.uploadFileError = '';
-            this.deletingPreviewImage = false;
-            this.uploadImageURL = null;
-        },
-        async menuAction(action) {
-            switch (action) {
-                case 'removeLock':
-                    this.displayRemoveLockDialog();
-                    break;
-                case 'editCurrentElement':
-                    await this.loadStructuralElement(this.currentId);
-                    if (this.blockedByAnotherUser) {
-                        this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
-
-                        return false;
-                    }
-                    try {
-                        await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
-                    } catch(error) {
-                        if (error.status === 409) {
-                            this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
-                        } else {
-                            console.log(error);
-                        }
-
-                        return false;
-                    }
-                    this.initCurrent();
-                    this.showElementEditDialog(true);
-                    break;
-                case 'addElement':
-                    this.errorEmptyChapterName = false;
-                    this.showElementAddDialog(true);
-                    break;
-                case 'deleteCurrentElement':
-                    await this.loadStructuralElement(this.currentId);
-                    if (this.blockedByAnotherUser) {
-                        this.companionInfo({
-                            info: this.$gettextInterpolate(
-                                this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Seite bearbeitet.'),
-                                {blockingUserName: this.blockingUserName}
-                            )
-                        });
-
-                        return false;
-                    }
-                    await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
-                    this.showElementDeleteDialog(true);
-                    break;
-                case 'showInfo':
-                    this.showElementInfoDialog(true);
-                    break;
-                case 'showExportOptions':
-                    this.showElementExportDialog(true);
-                    break;
-                case 'oerCurrentElement':
-                    this.showElementOerDialog(true);
-                    break;
-                case 'showSuggest':
-                    this.updateShowSuggestOerDialog(true);
-                    break;
-                case 'setBookmark':
-                    this.setBookmark();
-                    break;
-                case 'linkElement':
-                    this.showElementPublicLinkDialog(true);
-                    break;
-                case 'activateFullscreen':
-                    STUDIP.Fullscreen.activate();
-                    break;
-            }
-        },
-        async closeEditDialog() {
-            await this.loadStructuralElement(this.currentElement.id);
-            if (this.blockedByThisUser) {
-                await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
-                await this.loadStructuralElement(this.currentElement.id);
-            }
-            this.showElementEditDialog(false);
-            this.initCurrent();
-        },
-        closeAddDialog() {
-            this.showElementAddDialog(false);
-        },
-        checkUploadFile() {
-            const file = this.$refs?.upload_image?.files[0];
-            this.uploadImageURL = null;
-            this.uploadFileError = this.checkUploadImageFile(this.$refs?.upload_image?.files[0]);
-            if (this.uploadFileError === '') {
-                this.deletingPreviewImage = false;
-                this.uploadImageURL = window.URL.createObjectURL(file);
-            }
-        },
-        deleteImage() {
-            if (!this.deletingPreviewImage) {
-                this.deletingPreviewImage = true;
-            }
-        },
-        async storeCurrentElement() {
-            await this.loadStructuralElement(this.currentElement.id);
-            if (this.blockedByAnotherUser) {
-                this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
-                    )
-                });
-                this.showElementEditDialog(false);
-                return false;
-            }
-            if (!this.blocked) {
-                await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
-            }
-
-            const file = this.$refs?.upload_image?.files[0];
-            try {
-                this.uploadFileError = '';
-                if (file) {
-                    await this.uploadImageForStructuralElement({
-                        structuralElement: this.currentElement,
-                        file,
-                    });
-                } else if (this.selectedStockImage) {
-                    await this.setStockImageForStructuralElement({
-                        structuralElement: this.currentElement,
-                        stockImage: this.selectedStockImage,
-                    })
-                } else if (this.deletingPreviewImage) {
-                    await this.deleteImageForStructuralElement(this.currentElement);
-                }
-
-                this.loadStructuralElement(this.currentElement.id);
-            } catch(error) {
-                console.error(error);
-                this.uploadFileError = this.$gettext('Das Bild für das neue Lernmaterial konnte nicht gespeichert werden.');
-            }
-
-            this.showElementEditDialog(false);
-
-            if (this.currentElement.attributes['release-date'] !== '') {
-                this.currentElement.attributes['release-date'] =
-                    new Date(this.currentElement.attributes['release-date']).getTime() / 1000;
-            }
-
-            if (this.currentElement.attributes['withdraw-date'] !== '') {
-                this.currentElement.attributes['withdraw-date'] =
-                    new Date(this.currentElement.attributes['withdraw-date']).getTime() / 1000;
-            }
-
-            const element = {
-                id: this.currentElement.id,
-                type: this.currentElement.type,
-                attributes: this.currentElement.attributes,
-            };
-
-            await this.updateStructuralElement({ element, id: this.currentId});
-            await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
-            this.$emit('select', this.currentId);
-            this.initCurrent();
-        },
-
-        dropContainer() {
-            this.isDragging = false;
-            this.storeSort();
-        },
-
-        async storeSort() {
-            const timeout = setTimeout(() => this.processing = true, 800);
-            if (this.blockedByAnotherUser) {
-                this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
-                clearTimeout(timeout);
-                this.processing = false;
-                return false;
-            }
-            try {
-                await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
-            } catch (error) {
-                if (error.status === 409) {
-                    this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
-                } else {
-                    console.log(error);
-                }
-
-                clearTimeout(timeout);
-                this.processing = false;
-                return false;
-            }
-
-            await this.sortContainersInStructualElements({
-                structuralElement: this.structuralElement,
-                containers: this.containerList,
-            });
-            this.$emit('select', this.currentId);
-
-            clearTimeout(timeout);
-            this.processing = false;
-        },
-
-        async exportCurrentElement(data) {
-            if (this.exportRunning) {
-                return;
-            }
-
-            this.exportRunning = true;
-
-            await this.sendExportZip(this.currentElement.id, {
-                withChildren: this.exportChildren,
-            });
-
-            this.exportRunning = false;
-            this.showElementExportDialog(false);
-        },
-
-        pdfExportCurrentElement() {
-            this.showElementPdfExportDialog(false);
-            let url = '';
-            let withChildren = this.pdfExportChildren ? '/1' : '/0';
-            if (this.context.type === 'users') {
-                url = STUDIP.URLHelper.getURL('dispatch.php/contents/courseware/pdf_export/' + this.structuralElement.id + withChildren);
-            }
-            if (this.context.type === 'courses') {
-                url = STUDIP.URLHelper.getURL('dispatch.php/course/courseware/pdf_export/' + this.structuralElement.id + withChildren);
-            }
-
-            if (url) {
-                window.open(url , '_blank').focus();
-            }
-        },
-
-        async publishCurrentElement() {
-            if (this.oerExportRunning) {
-                return;
-            }
-            this.oerExportRunning = true;
-            await this.exportToOER(this.currentElement, { withChildren: this.oerChildren });
-            this.oerExportRunning = false;
-            this.showElementOerDialog(false);
-        },
-
-        async closeDeleteDialog() {
-            await this.loadStructuralElement(this.currentElement.id);
-            if (this.blockedByThisUser) {
-                await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
-            }
-            this.showElementDeleteDialog(false);
-        },
-        async deleteCurrentElement() {
-            await this.loadStructuralElement(this.currentElement.id);
-            if (this.blockedByAnotherUser) {
-                this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
-                    )
-                });
-                this.showElementDeleteDialog(false);
-                return false;
-            }
-            let parent_id = this.structuralElement.relationships.parent.data.id;
-            this.showElementDeleteDialog(false);
-            this.companionInfo({ info: this.$gettext('Lösche Seite und alle darunter liegenden Elemente.') });
-            this.deleteStructuralElement({
-                id: this.currentId,
-                parentId: this.structuralElement.relationships.parent.data.id,
-            })
-            .then(response => {
-                this.$router.push(parent_id);
-                this.companionInfo({ info: this.$gettext('Die Seite wurde gelöscht.') });
-            })
-            .catch(error => {
-                this.companionError({ info: this.$gettext('Die Seite konnte nicht gelöscht werden.') });
-            });
-        },
-        containerComponent(container) {
-            return 'courseware-' + container.attributes['container-type'] + '-container';
-        },
-        setBookmark() {
-            this.addBookmark(this.structuralElement);
-            this.companionInfo({ info: this.$gettext('Das Lesezeichen wurde gesetzt.') });
-        },
-        updateReadApproval(approval) {
-            this.currentElement.attributes['read-approval'] = approval;
-        },
-        updateWriteApproval(approval) {
-            this.currentElement.attributes['write-approval'] = approval;
-        },
-        sendOerSuggestion() {
-            this.suggestViaAction(this.currentElement, this.additionalText);
-            this.updateShowSuggestOerDialog(false);
-        },
-        async createElementPublicLink() {
-            const date = this.publicLink['expire-date'];
-            const publicLink = {
-                attributes: {
-                    password: this.publicLink.password,
-                    'expire-date': date === '' ? new Date(0).toISOString() : new Date(date).toISOString()
-                },
-                relationships: {
-                    'structural-element': {
-                        data: {
-                            id: this.currentElement.id,
-                            type: 'courseware-structural-elements'
-                        }
-                    }
-                }
-            }
-
-            await this.createLink({ publicLink });
-            this.companionSuccess({
-                info: this.$gettext('Öffentlicher Link wurde angelegt. Unter Freigaben finden Sie alle Ihre öffentlichen Links.'),
-            });
-            this.closePublicLinkDialog();
-        },
-        closePublicLinkDialog() {
-            this.publicLink = {
-                passsword: '',
-                'expire-date': ''
-            };
-            this.showElementPublicLinkDialog(false);
-        },
-        displayRemoveLockDialog() {
-            this.showElementRemoveLockDialog(true);
-        },
-        async executeRemoveLock() {
-            await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
-            await this.loadStructuralElement(this.currentElement.id);
-            this.showElementRemoveLockDialog(false);
-        },
-        updateContainerList() {
-            this.containerList = this.containers;
-            const containerRefs = this.$refs.containers;
-            for (let ref of containerRefs) {
-                ref.initCurrentData();
-            }
-        },
-        keyHandler(e, containerId) {
-            switch (e.keyCode) {
-                case 27: // esc
-                    this.abortKeyboardSorting(containerId);
-                    break;
-                case 13: // enter
-                    e.preventDefault();
-                    if (this.keyboardSelected) {
-                        this.storeKeyboardSorting(containerId);
-                    } else {
-                        this.keyboardSelected = containerId;
-                        const container = this.containerById({id: containerId});
-                        const index = this.containerList.findIndex(c => c.id === container.id);
-                        this.assistiveLive =
-                            this.$gettextInterpolate(
-                                this.$gettext('%{containerTitle} Abschnitt ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
-                                , {containerTitle: container.attributes.title, pos: index + 1, listLength: this.containerList.length}
-                            );
-                    }
-                    break;
-            }
-            if (this.keyboardSelected) {
-                switch (e.keyCode) {
-                    case 9: //tab
-                        this.abortKeyboardSorting(containerId);
-                        break;
-                    case 38: // up
-                        e.preventDefault();
-                        this.moveItemUp(containerId);
-                        break;
-                    case 40: // down
-                        e.preventDefault();
-                        this.moveItemDown(containerId);
-                        break;
-                }
-            }
-        },
-        moveItemUp(containerId) {
-            const currentIndex = this.containerList.findIndex(container => container.id === containerId);
-            if (currentIndex !== 0) {
-                const container = this.containerById({id: containerId});
-                const newPos = currentIndex - 1;
-                this.containerList.splice(newPos, 0, this.containerList.splice(currentIndex, 1)[0]);
-                this.assistiveLive =
-                    this.$gettextInterpolate(
-                        this.$gettext('%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {containerTitle: container.attributes.title, pos: newPos + 1, listLength: this.containerList.length}
-                    );
-            }
-        },
-        moveItemDown(containerId) {
-            const currentIndex = this.containerList.findIndex(container => container.id === containerId);
-            if (this.containerList.length - 1 > currentIndex) {
-                const container = this.containerById({id: containerId});
-                const newPos = currentIndex + 1;
-                this.containerList.splice(newPos, 0, this.containerList.splice(currentIndex, 1)[0]);
-                this.assistiveLive =
-                    this.$gettextInterpolate(
-                        this.$gettext('%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {containerTitle: container.attributes.title, pos: newPos + 1, listLength: this.containerList.length}
-                    );
-            }
-        },
-        abortKeyboardSorting(containerId) {
-            const container = this.containerById({id: containerId});
-            this.keyboardSelected = null;
-            this.assistiveLive =
-                this.$gettextInterpolate(
-                    this.$gettext('%{containerTitle} Abschnitt, Neuordnung abgebrochen.')
-                    , {containerTitle: container.attributes.title}
-                );
-            this.$emit('select', this.currentId);
-        },
-        storeKeyboardSorting(containerId) {
-            const container = this.containerById({id: containerId});
-            const currentIndex = this.containerList.findIndex(container => container.id === containerId);
-            this.keyboardSelected = null;
-            this.assistiveLive =
-                this.$gettextInterpolate(
-                    this.$gettext('%{containerTitle} Abschnitt, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.')
-                    , {containerTitle: container.attributes.title, pos: currentIndex + 1, listLength: this.containerList.length}
-                );
-            this.storeSort();
-        },
-        onSelectStockImage(stockImage) {
-            if (this.$refs?.upload_image) {
-                this.$refs.upload_image.value = null;
-            }
-            this.selectedStockImage = stockImage;
-            this.showStockImageSelector = false;
-            this.deletingPreviewImage = false;
-        },
-    },
-    created() {
-        this.pluginManager.registerComponentsLocally(this);
-    },
-
-    watch: {
-        async structuralElement() {
-            this.setCurrentElementId(this.structuralElement.id);
-            this.initCurrent();
-            if (this.isTask) {
-                this.loadTask({
-                    taskId: this.structuralElement.relationships.task.data.id,
-                });
-            }
-
-            if (this.isLink) {
-                this.loadStructuralElement(this.structuralElement.attributes['target-id']);
-            }
-
-            if (this.inCourse && this.courseware.attributes['sequential-progression'] && !this.userIsTeacher) {
-               this.loadProgresses();
-            }
-        },
-        containers() {
-            this.containerList = this.containers;
-        },
-        containerList() {
-            if (this.keyboardSelected) {
-                this.$nextTick(() => {
-                    const selected = this.$refs['sortableHandle' + this.keyboardSelected][0];
-                    selected.focus();
-                    selected.scrollIntoView({behavior: "smooth", block: "center"});
-                });
-            }
-        },
-        consumeMode(newState) {
-            this.consumModeTrap = newState;
-        },
-    },
-
-    // this line provides all the components to courseware plugins
-    provide: () => ({
-        containerComponents: ContainerComponents,
-        coursewarePluginComponents: CoursewarePluginComponents,
-    }),
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementComments.vue b/resources/vue/components/courseware/CoursewareStructuralElementComments.vue
deleted file mode 100644
index d1f48b5..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElementComments.vue
+++ /dev/null
@@ -1,139 +0,0 @@
-<template>
-    <section
-        class="cw-structural-element-comments"
-        :class="[emptyComments ? 'cw-structural-element-comments-empty' : '']"
-    >
-        <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
-        <div class="cw-structural-element-comments-items" v-show="!emptyComments" ref="commentsRef">
-            <courseware-talk-bubble
-                v-for="comment in comments"
-                :key="comment.id"
-                :payload="buildPayload(comment)"
-            />
-        </div>
-        <div class="cw-structural-element-comment-create">
-            <textarea v-model="createComment" :placeholder="placeHolder" spellcheck="true"></textarea>
-            <button class="button" @click="postComment"><translate>Senden</translate></button>
-        </div>
-    </section>
-</template>
-
-<script>
-import CoursewareTalkBubble from './CoursewareTalkBubble.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-structural-element-comments',
-    components: {
-        CoursewareTalkBubble,
-    },
-    props: {
-        structuralElement: Object,
-    },
-    data() {
-        return {
-            createComment: '',
-            placeHolder: this.$gettext('Stellen Sie eine Frage oder kommentieren Sie...'),
-            srMessage: ''
-        };
-    },
-    computed: {
-        ...mapGetters({
-            relatedUser: 'users/related',
-            userId: 'userId',
-            getComments: 'courseware-structural-element-comments/related',
-        }),
-        comments() {
-            const parent = {
-                type: this.structuralElement.type,
-                id: this.structuralElement.id,
-            };
-
-            return this.getComments({ parent, relationship: 'comments' });
-        },
-        emptyComments() {
-            if (this.comments === null || this.comments.length === 0) {
-                return true;
-            }
-
-            return false;
-        }
-    },
-    methods: {
-        ...mapActions({
-            createComments: 'courseware-structural-element-comments/create',
-            loadRelatedComments: 'courseware-structural-element-comments/loadRelated'
-        }),
-        async loadComments() {
-            const parent = {
-                type: this.structuralElement.type,
-                id: this.structuralElement.id,
-            };
-            await this.loadRelatedComments({
-                parent,
-                relationship: 'comments',
-                options: {
-                    include: 'user',
-                },
-            });
-        },
-        async postComment() {
-            this.updateSrMessage(this.$gettext('Kommentar gesendet'));
-            const data = {
-                attributes: {
-                    comment: this.createComment
-                },
-                relationships: {
-                    'structural-element': {
-                        data: {
-                            id: this.structuralElement.id,
-                            type: this.structuralElement.type
-                        }
-                    }
-                }
-            };
-
-            await this.createComments(data);
-            this.loadComments();
-            this.createComment = '';
-        },
-        buildPayload(comment) {
-            const commenter = this.relatedUser({
-                parent: { id: comment.id, type: comment.type },
-                relationship: 'user',
-            });
-
-            const payload = {
-                id: comment.id,
-                own: comment.relationships.user.data.id === this.userId,
-                content: comment.attributes.comment,
-                chdate: comment.attributes.chdate,
-                mkdate: comment.attributes.mkdate,
-                user_id: commenter.id,
-                user_name: commenter.attributes['formatted-name'],
-                user_avatar: commenter.meta.avatar.small,
-            };
-
-            return payload;
-        },
-        updateSrMessage(message) {
-            this.srMessage = '';
-            this.srMessage = message;
-        }
-    },
-    mounted() {
-        this.loadComments();
-    },
-    updated() {
-        let ref = this.$refs["commentsRef"];
-        ref.scrollTop = ref.scrollHeight;
-    },
-    watch: {
-        comments() {
-            if (this.comments && this.comments.length > 0) {
-                this.$emit('hasComments');
-            }
-        }
-    }
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDialogAdd.vue b/resources/vue/components/courseware/CoursewareStructuralElementDialogAdd.vue
deleted file mode 100644
index d182495..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElementDialogAdd.vue
+++ /dev/null
@@ -1,380 +0,0 @@
-<template>
-    <studip-wizard-dialog
-        :title="$gettext('Seite hinzufügen')"
-        :confirmText="$gettext('Erstellen')"
-        :closeText="$gettext('Abbrechen')"
-        :slots="wizardSlots"
-        :lastRequiredSlotId="1"
-        :requirements="requirements"
-        @close="closeAddDialog"
-        @confirm="createElement"
-    >
-        <template v-slot:basic>
-            <form class="default" @submit.prevent="">
-                <label>
-                    {{ $gettext('Position der neuen Seite') }}
-                    <select v-model="pageParent">
-                        <option v-if="!isRoot && canEditParent" value="sibling">
-                            {{ $gettext('Neben der aktuellen Seite') }}
-                        </option>
-                        <option value="descendant">{{ $gettext('Unterhalb der aktuellen Seite') }}</option>
-                    </select>
-                </label>
-                <label>
-                    <span>{{ text.title }}</span>
-                    <span aria-hidden="true" class="wizard-required">*</span>
-                    <input type="text" v-model="title" required />
-                </label>
-                <label>
-                    <span>{{ $gettext('Beschreibung') }}</span>
-                    <textarea v-model="description" required />
-                </label>
-            </form>
-        </template>
-        <template v-slot:template>
-            <form v-if="hasTemplates" class="default" @submit.prevent="">
-                <label>
-                    {{ $gettext('Art der Vorlage') }}
-                    <select v-model="templatePurpose">
-                        <option value="content">{{ $gettext('Inhalt') }}</option>
-                        <option value="oer">{{ $gettext('OER-Material') }}</option>
-                        <option value="portfolio">{{ $gettext('ePortfolio') }}</option>
-                        <option value="draft">{{ $gettext('Entwurf') }}</option>
-                        <option v-if="!inCourseContext" value="template">{{ $gettext('Aufgabenvorlage') }}</option>
-                        <option value="other">{{ $gettext('Sonstiges') }}</option>
-                    </select>
-                </label>
-                <label>
-                    <span>{{ $gettext('Vorlage') }}</span>
-                    <select v-model="selectedTemplate">
-                        <option :value="null">{{ $gettext('ohne Vorlage') }}</option>
-                        <option v-for="template in selectableTemplates" :key="template.id" :value="template">
-                            {{ template.attributes.name }}
-                        </option>
-                    </select>
-                </label>
-            </form>
-            <courseware-companion-box
-                v-else
-                :msgCompanion="$gettext('Es wurden keine Vorlagen gefunden.')"
-                mood="pointing"
-                class="cw-companion-box-in-form"
-            />
-        </template>
-        <template v-slot:layout>
-            <form class="default" @submit.prevent="">
-                <label>
-                    {{ $gettext('Bild') }}
-                    <input
-                        class="cw-file-input"
-                        ref="upload_image"
-                        type="file"
-                        accept="image/*"
-                        @change="checkUploadFile"
-                    />
-                    <courseware-companion-box
-                        v-if="uploadFileError"
-                        :msgCompanion="uploadFileError"
-                        mood="sad"
-                        class="cw-companion-box-in-form"
-                    />
-                </label>
-                <label>
-                    {{ $gettext('Farbe') }}
-                    <studip-select v-model="color" :options="colors" :reduce="(color) => color.class" label="class">
-                        <template #open-indicator="selectAttributes">
-                            <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span>
-                        </template>
-                        <template #no-options>
-                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                        </template>
-                        <template #selected-option="{ name, hex }">
-                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span>
-                            <span>{{ name }}</span>
-                        </template>
-                        <template #option="{ name, hex }">
-                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span>
-                            <span>{{ name }}</span>
-                        </template>
-                    </studip-select>
-                </label>
-            </form>
-        </template>
-        <template v-slot:advanced>
-            <form class="default" @submit.prevent="">
-                <label>
-                    {{ $gettext('Art des Lernmaterials') }}
-                    <select v-model="purpose">
-                        <option value="content">{{ $gettext('Inhalt') }}</option>
-                        <option value="oer">{{ $gettext('OER-Material') }}</option>
-                        <option value="portfolio">{{ $gettext('ePortfolio') }}</option>
-                        <option value="draft">{{ $gettext('Entwurf') }}</option>
-                        <option v-if="!inCourseContext" value="template">{{ $gettext('Aufgabenvorlage') }}</option>
-                        <option value="other">{{ $gettext('Sonstiges') }}</option>
-                    </select>
-                </label>
-                <label>
-                    {{ $gettext('Lizenztyp') }}
-                    <select v-model="license_type">
-                        <option v-for="license in licenses" :key="license.id" :value="license.id">
-                            {{ license.name }}
-                        </option>
-                    </select>
-                </label>
-                <label>
-                    {{ $gettext('Geschätzter zeitlicher Aufwand') }}
-                    <input type="text" v-model="required_time" />
-                </label>
-                <label>
-                    {{ $gettext('Niveau') }}<br />
-                    {{ $gettext('von') }}
-                    <select v-model="difficulty_start">
-                        <option v-for="difficulty_start in 12" :key="difficulty_start" :value="difficulty_start">
-                            {{ difficulty_start }}
-                        </option>
-                    </select>
-                    {{ $gettext('bis') }}
-                    <select v-model="difficulty_end">
-                        <option v-for="difficulty_end in 12" :key="difficulty_end" :value="difficulty_end">
-                            {{ difficulty_end }}
-                        </option>
-                    </select>
-                </label>
-            </form>
-        </template>
-    </studip-wizard-dialog>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import StudipSelect from './../StudipSelect.vue';
-import StudipWizardDialog from './../StudipWizardDialog.vue';
-import colorMixin from '@/vue/mixins/courseware/colors.js';
-import wizardMixin from '@/vue/mixins/courseware/wizard.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-structural-element-dialog-add',
-    mixins: [colorMixin, wizardMixin],
-    components: {
-        CoursewareCompanionBox,
-        StudipWizardDialog,
-        StudipSelect,
-    },
-    props: {
-        structuralElement: Object,
-        isRoot: Boolean,
-        canEditParent: Boolean,
-    },
-    data() {
-        return {
-            wizardSlots: [
-                {
-                    id: 1,
-                    valid: false,
-                    name: 'basic',
-                    title: this.$gettext('Grundeinstellungen'),
-                    icon: 'courseware',
-                    description: this.$gettext(
-                        'Wählen Sie einen kurzen, prägnanten Titel und beschreiben Sie in einigen Worten den Inhalt der Seite.'
-                    ),
-                },
-                {
-                    id: 2,
-                    valid: true,
-                    name: 'template',
-                    title: this.$gettext('Vorlage'),
-                    icon: 'content2',
-                    description: this.$gettext('Vorlagen enthalten Abschnitte und Blöcke, die bereits für bestimmte Zwecke angeordent sind. Beim anlegen der Seite, wird diese mit Abschnitten und Blöcken befüllt.'),
-                },
-                {
-                    id: 3,
-                    valid: true,
-                    name: 'layout',
-                    title: this.$gettext('Erscheinung'),
-                    icon: 'picture',
-                    description: this.$gettext(
-                        'Ein Vorschaubild motiviert Lernende die Seite zu erkunden. Die Kombination aus Bild und Farbe erleichtert das wiederfinden der Seite in einem Inhaltsverzeichnisblock.'
-                    ),
-                },
-                {
-                    id: 4,
-                    valid: true,
-                    name: 'advanced',
-                    title: this.$gettext('Zusatzangaben'),
-                    icon: 'info-list',
-                    description: this.$gettext(
-                        'Hier können Sie detaillierte Angaben zur Seite eintragen. Diese sind besonders interessant wenn die Seite als OER geteilt wird.'
-                    ),
-                },
-            ],
-            text: {
-                title: this.$gettext('Titel der neuen Seite'),
-            },
-            uploadFileError: '',
-            requirements: [],
-
-            pageParent: '',
-            title: '',
-            description: '',
-            purpose: '',
-            color: '',
-            license_type: '',
-            required_time: '',
-            difficulty_start: '',
-            difficulty_end: '',
-            templatePurpose: '',
-            selectedTemplate: null,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            licenses: 'licenses',
-            context: 'context',
-            lastCreatedStructuralElement: 'courseware-structural-elements/lastCreated',
-            structuralElementById: 'courseware-structural-elements/byId',
-            templates: 'courseware-templates/all',
-        }),
-        inCourseContext() {
-            return this.context.type === 'courses';
-        },
-        colors() {
-            return this.mixinColors.filter((color) => color.darkmode);
-        },
-        selectableTemplates() {
-            return this.templates.filter((template) => {
-                return template.attributes.purpose === this.templatePurpose;
-            });
-        },
-        hasTemplates() {
-            return this.templates.length > 0;
-        },
-    },
-    methods: {
-        ...mapActions({
-            createStructuralElementWithTemplate: 'createStructuralElementWithTemplate',
-            updateStructuralElement: 'updateStructuralElement',
-            companionError: 'companionError',
-            companionInfo: 'companionInfo',
-            companionSuccess: 'companionSuccess',
-            showElementAddDialog: 'showElementAddDialog',
-            loadStructuralElementById: 'courseware-structural-elements/loadById',
-            uploadImageForStructuralElement: 'uploadImageForStructuralElement',
-        }),
-        initWizardData() {
-            this.pageParent = 'descendant';
-            this.title = '';
-            this.description = '';
-            this.purpose = 'content';
-            this.color = 'studip-blue';
-            this.license_type = '';
-            this.required_time = '';
-            this.difficulty_start = '';
-            this.difficulty_end = '';
-            this.templatePurpose = 'content';
-            this.selectedTemplate = null;
-            this.requirements.push({ slot: this.wizardSlots[0], text: this.text.title });
-        },
-        closeAddDialog() {
-            this.showElementAddDialog(false);
-            this.initWizardData();
-        },
-        checkUploadFile() {
-            this.uploadFileError = this.checkUploadImageFile(this.$refs?.upload_image?.files[0]);
-        },
-        async createElement() {
-            let parent_id = this.structuralElement.id; // new page is descandant as default
-
-            this.errorEmptyChapterName = this.title.trim();
-            if (this.errorEmptyChapterName === '') {
-                return;
-            }
-            if (this.pageParent === 'sibling') {
-                parent_id = this.structuralElement.relationships.parent.data.id;
-            }
-            this.showElementAddDialog(false);
-
-            const file = this.$refs?.upload_image?.files[0];
-            const element = {
-                attributes: {
-                    title: this.title,
-                    purpose: this.purpose,
-                    payload: {
-                        description: this.description,
-                        color: this.color,
-                        license_type: this.license_type,
-                        required_time: this.required_time,
-                        difficulty_start: this.difficulty_start,
-                        difficulty_end: this.difficulty_end,
-                    },
-                },
-                templateId: this.selectedTemplate ? this.selectedTemplate.id : null,
-                parentId: parent_id,
-                currentId: this.structuralElement.id,
-            };
-
-            try {
-                await this.createStructuralElementWithTemplate(element);
-            } catch (e) {
-                let errorMessage = this.$gettext(
-                    'Es ist ein Fehler aufgetreten. Die Seite konnte nicht erstellt werden.'
-                );
-                if (e.status === 403) {
-                    errorMessage = this.$gettext(
-                        'Die Seite konnte nicht erstellt werden. Sie haben nicht die notwendigen Schreibrechte.'
-                    );
-                }
-
-                this.companionError({ info: errorMessage });
-                return;
-            }
-
-            const newCreated = this.lastCreatedStructuralElement;
-            await this.loadStructuralElementById({ id: newCreated.id });
-            const newElement = this.structuralElementById({ id: newCreated.id });
-            this.companionSuccess({
-                info: this.$gettextInterpolate(this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'), {
-                    pageTitle: newElement.attributes.title,
-                }),
-            });
-
-            if (file && this.uploadFileError === '') {
-                try {
-                    await this.uploadImageForStructuralElement({ structuralElement: newElement, file });
-                } catch (error) {
-                    console.error(error);
-                    this.companionError({
-                        info: this.$gettext('Das Bild für das neue Lernmaterial konnte nicht gespeichert werden.'),
-                    });
-                }
-                this.loadStructuralElementById({ id: newElement.id, options: { include: 'children' } });
-            }
-            this.initWizardData();
-            this.$router.push(newElement.id);
-        },
-    },
-    mounted() {
-        this.initWizardData();
-        if (!this.hasTemplates) {
-                this.wizardSlots.splice(1,1);
-                this.wizardSlots[1]['id'] = 2;
-                this.wizardSlots[2]['id'] = 3;
-        }
-    },
-    watch: {
-        title(newTitle) {
-            this.requirements = [];
-            const slot = this.wizardSlots[0];
-            if (newTitle === '') {
-                slot.valid = false;
-                this.requirements.push({ slot: slot, text: this.text.title });
-            } else {
-                slot.valid = true;
-            }
-        },
-        templatePurpose(newPurpose) {
-            this.selectedTemplate = null;
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue b/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue
deleted file mode 100644
index 76d84c8..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue
+++ /dev/null
@@ -1,477 +0,0 @@
-<template>
-    <studip-wizard-dialog
-        :title="$gettext('Seiten kopieren')"
-        :confirmText="$gettext('Kopieren')"
-        :closeText="$gettext('Abbrechen')"
-        :lastRequiredSlotId="3"
-        :requirements="requirements"
-        :slots="wizardSlots"
-        @close="showElementCopyDialog(false)"
-        @confirm="copyElement"
-    >
-        <template v-slot:source>
-            <form class="default" @submit.prevent="">
-                <fieldset class="radiobutton-set">
-                    <input
-                        id="cw-element-copy-source-self"
-                        v-if="inCourseContext"
-                        type="radio"
-                        v-model="source"
-                        value="self"
-                        :aria-description="text.sourceSelf"
-                    />
-                    <label v-if="inCourseContext" @click="source = 'self'" for="cw-element-copy-source-self">
-                        <div class="icon"><studip-icon shape="seminar" :size="32"/></div>
-                        <div class="text">{{ text.sourceSelf }}</div>
-                        <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
-                        <studip-icon shape="check-circle" :size="24" class="check" />
-                    </label>
-                    <input
-                        id="cw-element-copy-source-courses"
-                        type="radio"
-                        v-model="source"
-                        value="courses"
-                        :aria-description="text.sourceCourses"
-                    />
-                    <label @click="source = 'courses'" for="cw-element-copy-source-courses">
-                        <div class="icon"><studip-icon shape="seminar" :size="32"/></div>
-                        <div class="text">{{ text.sourceCourses }}</div>
-                        <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
-                        <studip-icon shape="check-circle" :size="24" class="check" />
-                    </label>
-                    <input
-                        id="cw-element-copy-source-users"
-                        type="radio"
-                        v-model="source"
-                        value="users"
-                        :aria-description="text.sourceUsers"
-                    />
-                    <label @click="source = 'users'" for="cw-element-copy-source-users">
-                        <div class="icon"><studip-icon shape="content" :size="32"/></div>
-                        <div class="text">{{ text.sourceUsers }}</div>
-                        <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
-                        <studip-icon shape="check-circle" :size="24" class="check" />
-                    </label>
-                </fieldset>
-                <template v-if="source === 'courses'">
-                    <label>
-                        <span>{{ $gettext('Semester') }}</span><span aria-hidden="true"></span>
-                        <select v-model="selectedSemester">
-                            <option value="all">{{ $gettext('Alle Semester') }}</option>
-                            <option v-for="semester in semesterMap" :key="semester.id" :value="semester.id">
-                                {{ semester.attributes.title }}
-                            </option>
-                        </select>
-                    </label>
-                    <label>
-                        <span>{{ $gettext('Veranstaltung') }}</span><span aria-hidden="true" class="wizard-required">*</span>
-                        <studip-select
-                            v-if="filteredCourses.length !== 0 && !loadingCourses"
-                            :options="filteredCourses"
-                            :clearable="false"
-                            :reduce="option => option.id"
-                            :getOptionLabel="option => option.attributes.title"
-                            v-model="selectedRange"
-                        >
-                            <template #open-indicator="selectAttributes">
-                                <span v-bind="selectAttributes"
-                                    ><studip-icon shape="arr_1down" :size="10"
-                                /></span>
-                            </template>
-                        </studip-select>
-                        <p v-if="loadingCourses">
-                            {{$gettext('Lade Veranstaltungen…')}}
-                        </p>
-                        <p v-if="filteredCourses.length === 0 && !loadingCourses">
-                            {{$gettext('Es wurden keine geeigneten Veranstaltungen gefunden.')}}
-                        </p>
-                    </label>
-                </template>
-
-            </form>
-        </template>
-        <template v-slot:unit>
-            <form class="default" @submit.prevent="">
-                <fieldset v-if="units.length !== 0" class="radiobutton-set">
-                    <template v-for="unit in units">
-                        <input
-                            :id="'cw-element-copy-unit-' + unit.id"
-                            type="radio"
-                            v-model="selectedUnit"
-                            :checked="unit.id === selectedUnitId"
-                            :value="unit"
-                            :key="'radio-' + unit.id"
-                            :aria-description="unit.element.attributes.title"
-                        />
-                        <label :key="'label-' + unit.id" :for="'cw-element-copy-unit-' + unit.id">
-                            <div class="icon"><studip-icon shape="courseware" :size="32"/></div>
-                            <div class="text">{{ unit.element.attributes.title }}</div>
-                            <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
-                            <studip-icon shape="check-circle" :size="24" class="check" />
-                        </label>
-                    </template>
-                </fieldset>
-                <courseware-companion-box
-                    v-else
-                    mood="sad"
-                    :msgCompanion="$gettext('Für die gewählte Quelle stehen kein Lernmaterialien zur Verfügung.')"
-                />
-            </form>
-        </template>
-        <template v-slot:element>
-            <form v-if="selectedUnit" class="default" @submit.prevent="">
-                <courseware-structural-element-selector
-                    v-model="selectedElement"
-                    :rootId="selectedUnitRootId"
-                    :validateAncestors="true"
-                    :targetId="currentElement"
-                />
-            </form>
-            <courseware-companion-box
-                v-else
-                mood="pointing"
-                :msgCompanion="$gettext('Bitte wählen Sie ein Lernmaterial aus.')"
-            />
-        </template>
-        <template v-slot:edit>
-            <form v-if="selectedUnit" class="default" @submit.prevent="">
-                <label>
-                    {{$gettext('Titel')}}
-                    <input type="text" v-model="modifiedTitle" required />
-                </label>
-                <label>
-                    {{$gettext('Farbe')}}
-                    <studip-select
-                        v-model="modifiedColor"
-                        :options="colors"
-                        :reduce="(color) => color.class"
-                        :clearable="false"
-                        label="class"
-                    >
-                        <template #open-indicator="selectAttributes">
-                            <span v-bind="selectAttributes"
-                                ><studip-icon shape="arr_1down" :size="10"
-                            /></span>
-                        </template>
-                        <template #no-options>
-                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                        </template>
-                        <template #selected-option="{ name, hex }">
-                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                            ><span>{{ name }}</span>
-                        </template>
-                        <template #option="{ name, hex }">
-                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                            ><span>{{ name }}</span>
-                        </template>
-                    </studip-select>
-                </label>
-                <label>
-                    {{$gettext('Beschreibung')}}
-                    <textarea v-model="modifiedDescription" required />
-                </label>
-            </form>
-            <courseware-companion-box
-                    v-else
-                    mood="pointing"
-                    :msgCompanion="$gettext('Bitte wählen Sie eine Seite aus.')"
-            />
-        </template>
-    </studip-wizard-dialog>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareStructuralElementSelector from './CoursewareStructuralElementSelector.vue';
-import colorMixin from '@/vue/mixins/courseware/colors.js';
-import StudipSelect from './../StudipSelect.vue';
-import StudipWizardDialog from './../StudipWizardDialog.vue';
-
-import { mapActions, mapGetters } from 'vuex'
-
-export default {
-    name: 'courseware-structural-element-dialog-copy',
-    mixins: [colorMixin],
-    components: {
-        CoursewareCompanionBox,
-        CoursewareStructuralElementSelector,
-        StudipWizardDialog,
-        StudipSelect,
-    },
-    data() {
-        return {
-            wizardSlots: [
-                { id: 1, valid: false, name: 'source', title: this.$gettext('Quelle'), icon: 'source',
-                  description: this.$gettext('Wählen Sie hier den Ort in Stud.IP aus, an dem sich der zu kopierende Lerninhalt befindet.') },
-                { id: 2, valid: false, name: 'unit', title: this.$gettext('Lernmaterial'), icon: 'courseware',
-                  description: this.$gettext('Wählen Sie das Lernmaterial aus, in dem sich der zu kopierende Lerninhalt befindet.') },
-                { id: 3, valid: false, name: 'element', title: this.$gettext('Seite'), icon: 'content2',
-                  description: this.$gettext('Wählen Sie die zu kopierende Seite aus. Um Unterseiten anzuzeigen, klicken Sie auf den Seitennamen. Mit einem weiteren Klick werden die Unterseiten wieder zugeklappt.') },
-                { id: 4, valid: true, name: 'edit', title: this.$gettext('Anpassen'), icon: 'edit',
-                  description: this.$gettext('Sie können hier die Daten der zu kopierenden Seite anpassen. Eine Anpassung ist optional, Sie können die Seite auch unverändert kopieren.') },
-            ],
-            source: '',
-            loadingCourses: false,
-            courses: [],
-            semesterMap: [],
-            selectedSemester: 'all',
-            selectedRange: '',
-            loadingUnits: false,
-            selectedUnit: null,
-            selectedElement: null,
-            modifiedTitle: '',
-            modifiedColor: '',
-            modifiedDescription: '',
-            requirements: [],
-            text: {
-                sourceSelf: this.$gettext('Diese Veranstaltung'),
-                sourceCourses: this.$gettext('Veranstaltung'),
-                sourceUsers: this.$gettext('Arbeitsplatz'),
-                source: this.$gettext('Quelle'),
-                unit: this.$gettext('Lernmaterial'),
-                element: this.$gettext('Seite'),
-            },
-        }
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            coursewareUnits: 'courseware-units/all',
-            semesterById: 'semesters/byId',
-            structuralElementById: 'courseware-structural-elements/byId',
-            context: 'context',
-            childrenById: 'courseware-structure/children',
-            currentElement: 'currentElement'
-        }),
-        colors() {
-            return this.mixinColors.filter(color => color.darkmode);
-        },
-        inCourseContext() {
-            return this.context.type === 'courses';
-        },
-        units() {
-            let units = this.coursewareUnits.filter(unit => unit.relationships.range.data.id === this.selectedRange);
-            units.forEach(unit => {
-                unit.element = this.getUnitElement(unit);
-            });
-
-            return units;
-        },
-        selectedUnitId() {
-            return this.selectedUnit?.id;
-        }, 
-        selectedUnitRootId() {
-            return this.selectedUnit?.relationships?.['structural-element']?.data?.id;
-        }, 
-        selectedElementTitle() {
-            return this.selectedElement?.attributes?.title;
-        },
-        selectedElementParent() {
-            let parentData = this.selectedElement?.relationships?.parent?.data;
-            if (parentData){
-                return this.structuralElementById({id: parentData.id});
-            }
-
-            return null;
-        },
-        selectedElementParentTitle() {
-            if (this.selectedElementParent) {
-                return this.selectedElementParent.attributes.title;
-            }
-
-            return '';
-        },
-        children() {
-            if (!this.selectedElement) {
-                return [];
-            }
-
-            return this.childrenById(this.selectedElement.id)
-                .map((id) => this.structuralElementById({ id }))
-                .filter(Boolean);
-        },
-        filteredCourses() {
-            const courses = this.courses.filter((course) => { return course.id !== this.context.id });
-            if (this.selectedSemester === 'all') {
-                return courses;
-            } else {
-                return courses.filter((course) => {
-                    return course.relationships['start-semester'].data.id === this.selectedSemester;
-                });
-            }
-        }
-    },
-    mounted() {
-        this.initWizardData();
-    },
-    methods: {
-        ...mapActions({
-            showElementCopyDialog: 'showElementCopyDialog',
-            loadCourseUnits: 'loadCourseUnits',
-            loadUserUnits: 'loadUserUnits',
-            loadUsersCourses: 'loadUsersCourses',
-            loadSemester: 'semesters/loadById',
-            loadStructuralElement: 'courseware-structural-elements/loadById',
-            copyStructuralElement: 'copyStructuralElement',
-            companionError: 'companionError',
-            companionSuccess: 'companionSuccess',
-        }),
-        initWizardData() {
-            this.source = this.inCourseContext ? 'self' : 'users';
-            this.selectedRange = '';
-            this.selectedUnit = null;
-        },
-        getUnitElement(unit) {
-            return this.structuralElementById({id: unit.relationships['structural-element'].data.id});
-        },
-        async updateCourses() {
-            this.loadingCourses = true;
-            this.courses = await this.loadUsersCourses({ userId: this.userId, withCourseware: true });
-            this.loadSemesterMap();
-            this.loadingCourses = false;
-        },
-        loadSemesterMap() {
-            let view = this;
-            let semesters = [];
-            this.courses.every(course => {
-                let semId = course.relationships['start-semester'].data.id;
-                if(!semesters.includes(semId)) {
-                    semesters.push(semId);
-                }
-                return true;
-            });
-            semesters.every(semester => {
-                view.loadSemester({id: semester}).then( () => {
-                    view.semesterMap.push(view.semesterById({id: semester}));
-                    view.semesterMap.sort((a, b) => new Date(b.attributes.start) - new Date(a.attributes.start));
-                });
-                return true;
-            });
-        },
-        async updateCourseUnits(cid) {
-            this.loadingUnits = true;
-            await this.loadCourseUnits(cid);
-            this.loadingUnits = false;
-        },
-        async updateUserUnits() {
-            this.loadingUnits = true;
-            await this.loadUserUnits(this.userId);
-            this.loadingUnits = false;
-        },
-        selectElement(id) {
-            this.selectedElement = this.structuralElementById({id: id});
-            this.loadStructuralElement({id: id, options: {include: 'children'}});
-        },
-        setElementData() {
-            this.modifiedTitle = this.selectedElement.attributes.title;
-            this.modifiedColor = this.selectedElement.attributes.payload.color;
-            this.modifiedDescription = this.selectedElement.attributes.payload.description;
-        },
-        resetElementData() {
-            this.modifiedTitle = '';
-            this.modifiedColor = '';
-            this.modifiedDescription = '';
-        },
-        copyElement() {
-            let view = this;
-            this.copyStructuralElement({
-                    parentId: this.currentElement,
-                    elementId: this.selectedElement.id,
-                    migrate: false,
-                    modifications: {
-                        title: view.modifiedTitle,
-                        color: view.modifiedColor,
-                        description: view.modifiedDescription
-                    }
-            })
-            .then( () => {
-                view.companionSuccess({
-                    info: view.$gettextInterpolate(
-                        view.$gettext('Die Seite %{ pageTitle } wurde erfolgreich kopiert.'),
-                        {pageTitle: view.selectedElementTitle}
-                    )
-                });
-            })
-            .catch(error => {
-                view.companionError({
-                    info: view.$gettextInterpolate(
-                        view.$gettext('Die Seite %{ pageTitle } konnte nicht kopiert werden.'),
-                        {pageTitle: view.selectedElementTitle}
-                    )
-                });
-            })
-            .finally(() => {
-                view.showElementCopyDialog(false);
-            });
-        },
-        validateSelection() {
-            this.requirements = [];
-            if (this.selectedRange === '') {
-                this.requirements.push({slot: this.wizardSlots[0], text: this.text.source });
-            }
-            if (this.selectedUnit === null) {
-                this.requirements.push({slot: this.wizardSlots[1], text: this.text.unit });
-            }
-            if (this.selectedUnit === null) {
-                this.requirements.push({slot: this.wizardSlots[2], text: this.text.element });
-            }
-        }
-    },
-    watch: {
-        selectedElement(newElement) {
-            this.validateSelection();
-            if (newElement !== null) {
-                this.wizardSlots[2].valid = true;
-                this.setElementData();
-            } else {
-                this.resetElementData();
-                this.wizardSlots[2].valid = false;
-            }
-            
-        },
-        async selectedUnit(newUnit) {
-            this.validateSelection();
-            if (newUnit !== null) {
-                this.wizardSlots[1].valid = true;
-                await this.loadStructuralElement({id: this.selectedUnitRootId, options: {include: 'children'}});
-                this.selectedElement = null;
-            } else {
-                this.wizardSlots[1].valid = false;
-            }
-            
-        },
-        selectedRange(newRid) {
-            this.validateSelection();
-            this.selectedUnit = null;
-            if (newRid !== '') {
-                this.wizardSlots[0].valid = true;
-                if (this.source === 'courses' || this.source === 'self') {
-                    this.updateCourseUnits(newRid);
-                }
-                if (this.source === 'users') {
-                    this.loadUserUnits(newRid);
-                }
-            } else {
-                this.wizardSlots[0].valid = false;
-            }
-        },
-        source(newSource) {
-            switch (newSource) {
-                case 'self':
-                    this.selectedRange = this.context.id;
-                    break;
-                case 'courses':
-                    this.selectedRange = '';
-                    this.updateCourses();
-                    break;
-                case 'users':
-                    this.selectedRange = this.userId;
-                    break;
-            }
-        },
-        selectedSemester(newSemester) {
-            this.selectedRange = '';
-        }
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDialogImport.vue b/resources/vue/components/courseware/CoursewareStructuralElementDialogImport.vue
deleted file mode 100644
index 3c3974d..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElementDialogImport.vue
+++ /dev/null
@@ -1,200 +0,0 @@
-<template>
-    <studip-dialog
-        :title="$gettext('Seiten importieren')"
-        :confirmText="$gettext('Importieren')"
-        :confirmDisabled="importRunning || importAborted"
-        :closeText="importRunning || importAborted ? $gettext('Schließen') : $gettext('Abbrechen')"
-        height="420"
-        @close="showElementImportDialog(false)"
-        @confirm="importCoursewareArchiv"
-    >
-        <template v-slot:dialogContent>
-            <form v-if="!importRunning && !importAborted" class="default" @submit.prevent="">
-                <label>
-                    {{$gettext('Importdatei')}}
-                    <input class="cw-file-input" ref="importFile" type="file" accept=".zip" @change="setImport" />
-                </label>
-                <label>
-                {{$gettext('Importverhalten')}}
-                <select v-model="importBehavior">
-                    <option value="default">{{$gettext('Inhalte anhängen')}}</option>
-                    <option value="migrate">{{$gettext('Inhalte zusammenführen')}}</option>
-                </select>
-            </label>
-            </form>
-            <div role="status" aria-live="polite">
-                <courseware-companion-box 
-                    v-show="importDone && importErrors.length === 0"
-                    :msgCompanion="$gettext('Import erfolgreich!')"
-                    mood="special"
-                />
-                <courseware-companion-box
-                    v-show="importDone && importErrors.length > 0"
-                    :msgCompanion="$gettext('Import abgeschlossen. Es sind Fehler aufgetreten!')"
-                    mood="unsure"
-                />
-                <courseware-companion-box
-                    v-show="!importDone && importRunning"
-                    :msgCompanion="$gettext('Import läuft. Bitte schließen Sie den Dialog nicht bis der Import abgeschlossen wurde.')"
-                    mood="pointing"
-                />
-                <courseware-companion-box
-                    v-show="importAborted"
-                    :msgCompanion="$gettext('Import abgebrochen. Es sind Fehler aufgetreten!')"
-                    mood="sad"
-                />
-            </div>
-            <form v-if="!importDone && importRunning" class="default" @submit.prevent="">
-                <fieldset>
-                    <div v-if="!fileImportDone" class="cw-import-zip">
-                        <header>{{$gettext('Importiere Dateien')}}:</header>
-                        <div class="progress-bar-wrapper">
-                            <div class="progress-bar" role="progressbar" :style="{width: importFilesProgress + '%'}" :aria-valuenow="importFilesProgress" aria-valuemin="0" aria-valuemax="100">{{ importFilesProgress }}%</div>
-                        </div>
-                        {{ importFilesState }}
-                    </div>
-                    <div v-if="fileImportDone" class="cw-import-zip">
-                        <header>{{$gettext('Importiere Elemente')}}:</header>
-                        <div class="progress-bar-wrapper">
-                            <div class="progress-bar" role="progressbar" :style="{width: importStructuresProgress + '%'}" :aria-valuenow="importStructuresProgress" aria-valuemin="0" aria-valuemax="100">{{ importStructuresProgress }}%</div>
-                        </div>
-                        {{ importStructuresState }}
-                    </div>
-                </fieldset>
-            </form>
-            <form v-if="importErrors.length > 0" class="default" @submit.prevent="">
-                <fieldset>
-                    <legend>{{$gettext('Fehlermeldungen')}}</legend>
-                    <ul>
-                        <li v-for="(error, index) in importErrors" :key="index"> {{error}} </li>
-                    </ul>
-                </fieldset>
-            </form>
-        </template>
-    </studip-dialog>
-</template>
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareImport from '@/vue/mixins/courseware/import.js';
-
-import { mapActions, mapGetters } from 'vuex'
-import JSZip from 'jszip';
-
-export default {
-    name: 'courseware-structural-element-dialog-import',
-    components: {
-        CoursewareCompanionBox,
-    },
-    mixins: [CoursewareImport],
-    props: {
-        colors: Array
-    },
-    data() {
-        return {
-            importBehavior: 'default',
-            importRunning: false,
-            importZipFile: null,
-            zip: null,
-            importAborted: false,
-        }
-    },
-    computed: {
-        ...mapGetters({
-            currentElement: 'currentElement',
-            importFilesState: 'importFilesState',
-            importFilesProgress: 'importFilesProgress',
-            importStructuresState: 'importStructuresState',
-            importStructuresProgress: 'importStructuresProgress',
-            importErrors: 'importErrors',
-        }),
-        fileImportDone() {
-            return this.importFilesProgress === 100;
-        },
-        importDone() {
-            return (this.importFilesProgress === 100 && this.importStructuresProgress === 100);
-        }
-    },
-    methods: {
-        ...mapActions({
-            showElementImportDialog: 'showElementImportDialog',
-            loadCoursewareStructure: 'courseware-structure/load',
-            setImportFilesProgress: 'setImportFilesProgress',
-            setImportStructuresProgress: 'setImportStructuresProgress',
-            setImportErrors: 'setImportErrors',
-        }),
-        setImport(event) {
-            this.importZipFile = event.target.files[0];
-            this.setImportFilesProgress(0);
-            this.setImportStructuresProgress(0);
-            this.setImportErrors([]);
-        },
-        async importCoursewareArchiv() {
-            this.importAborted = false;
-            if (this.importZipFile === null) {
-                return false;
-            }
-
-            this.importRunning = true;
-            try {
-                this.zip = await JSZip.loadAsync(this.importZipFile);
-            } catch(error) {
-                this.setImportErrors([this.$gettext('Die gewählte Datei ist kein Archiv oder das Archiv ist beschädigt.')]);
-                this.importRunning = false;
-                this.importAborted = true;
-                return;
-            }
-            let errors = [];
-            let missingFiles = false;
-            if (this.zip.file('courseware.json') === null) {
-                errors.push(this.$gettext('Das Archiv enthält keine courseware.json Datei.'));
-                missingFiles = true;
-            }
-            if (this.zip.file('files.json') === null) {
-                errors.push(this.$gettext('Das Archiv enthält keine files.json Datei.'));
-                missingFiles = true;
-            }
-            if (this.zip.file('data.xml') !== null) {
-                errors.push(this.$gettext(
-                    'Das Archiv enthält eine data.xml Datei. Möglicherweise handelt es sich um einen Export aus dem Courseware-Plugin. Diese Archive sind nicht kompatibel mit dieser Courseware.'
-                ));
-            }
-            if (missingFiles) {
-                this.setImportErrors(errors);
-                this.importRunning = false;
-                this.importAborted = true;
-                return;
-            }
-
-            const data = await this.zip.file('courseware.json').async('string');
-            let courseware = null;
-            const data_files = await this.zip.file('files.json').async('string');
-            let files = null;
-            let jsonErrors = false;
-            try {
-                courseware = JSON.parse(data);
-            } catch (error) {
-                jsonErrors = true;
-                errors.push(this.$gettext('Die Beschreibung der Courseware-Inhalte ist nicht valide.'));
-                errors.push(error);
-            }
-            try {
-                files = JSON.parse(data_files);
-            } catch (error) {
-                jsonErrors = true;
-                errors.push(this.$gettext('Die Beschreibung der Dateien ist nicht valide.'));
-                errors.push(error);
-            }
-            if (jsonErrors) {
-                this.setImportErrors(errors);
-                this.importRunning = false;
-                this.importAborted = true;
-                return;
-            }
-
-            await this.loadCoursewareStructure();
-
-            await this.importCourseware(courseware, this.currentElement, files, this.importBehavior, null);
-        }
-    }
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue b/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue
deleted file mode 100644
index 78c6725..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue
+++ /dev/null
@@ -1,232 +0,0 @@
-<template>
-    <studip-wizard-dialog
-        :title="$gettext('Seiten verknüpfen')"
-        :confirmText="$gettext('Verknüpfen')"
-        :closeText="$gettext('Abbrechen')"
-        :lastRequiredSlotId="2"
-        :requirements="requirements"
-        :slots="wizardSlots"
-        @close="showElementLinkDialog(false)"
-        @confirm="linkElement"
-    >
-        <template v-slot:unit>
-            <form v-if="!loadingUnits" class="default" @submit.prevent="">
-                <fieldset v-if="hasUnits" class="radiobutton-set">
-                    <template v-for="unit in units">
-                        <input
-                            :id="'cw-element-link-unit-' + unit.id"
-                            type="radio"
-                            :checked="unit.id === selectedUnitId"
-                            :value="unit.id"
-                            :key="'radio-' + unit.id"
-                            :aria-description="unit.element.attributes.title"
-                        />
-                        <label @click="selectedUnit = unit" :key="'label-' + unit.id" :for="'cw-element-link-unit-' + unit.id">
-                            <div class="icon"><studip-icon shape="courseware" size="32"/></div>
-                            <div class="text">{{ unit.element.attributes.title }}</div>
-                            <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
-                            <studip-icon shape="check-circle" size="24" class="check" />
-                        </label>
-                    </template>
-                </fieldset>
-                <courseware-companion-box
-                    v-else
-                    mood="sad"
-                    :msgCompanion="$gettext('Es konnte leider kein Lernmaterial gefunden werden. Bitte erstellen Sie unter Arbeitsplatz/Courseware ein Lernmaterial.')"
-                />
-            </form>
-            <studip-progress-indicator 
-                v-else
-                :description="$gettext('Lade Lernmaterialien…')"
-            />
-        </template>
-        <template v-slot:element>
-            <form v-if="selectedUnit" class="default" @submit.prevent="">
-                <courseware-structural-element-selector
-                    v-model="selectedElement"
-                    :rootId="selectedUnitRootId"
-                />
-            </form>
-            <courseware-companion-box
-                v-else
-                mood="pointing"
-                :msgCompanion="$gettext('Bitte wählen Sie zuerst das Lernmaterial aus.')"
-            />
-        </template>
-    </studip-wizard-dialog>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareStructuralElementSelector from './CoursewareStructuralElementSelector.vue';
-import StudipWizardDialog from './../StudipWizardDialog.vue';
-import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-
-import { mapActions, mapGetters } from 'vuex'
-
-export default {
-    name: 'courseware-structural-element-dialog-link',
-    components: {
-        CoursewareCompanionBox,
-        CoursewareStructuralElementSelector,
-        StudipWizardDialog,
-        StudipProgressIndicator
-    },
-    data() {
-        return {
-            wizardSlots: [
-                {id: 1, valid: false, name: 'unit', title: this.$gettext('Lernmaterial'), icon: 'courseware', 
-                description: this.$gettext('Wählen Sie das Lernmaterial aus, in dem sich der zu verknüpfende Lerninhalt befindet. Die Lerninhalte, die verknüpft werden können, müssen unter Arbeitsplatz/Courseware vorher erstellt werden.')},
-                {id: 2, valid: false, name: 'element', title: this.$gettext('Seite'), icon: 'content2', 
-                description: this.$gettext('Wählen Sie die zu verknüpfende Seite aus. Um Unterseiten anzuzeigen, klicken Sie auf den Seitennamen. Mit einem weiteren Klick werden die Unterseiten wieder zugeklappt.')},            ],
-            loadingUnits: false,
-            selectedUnit: null,
-            selectedElement: null,
-            requirements: [],
-            text: {
-
-            }
-        }
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            coursewareUnits: 'courseware-units/all',
-            structuralElementById: 'courseware-structural-elements/byId',
-            context: 'context',
-            childrenById: 'courseware-structure/children',
-            currentElement: 'currentElement'
-        }),
-        units() {
-            let units = this.coursewareUnits.filter(unit => unit.relationships.range.data.id === this.userId);
-            units.forEach(unit => {
-                unit.element = this.getUnitElement(unit);
-            });
-
-            return units;
-        },
-        hasUnits() {
-            return this.units.length !== 0;
-        },
-        selectedUnitId() {
-            return this.selectedUnit?.id;
-        }, 
-        selectedUnitRootId() {
-            return this.selectedUnit?.relationships?.['structural-element']?.data?.id;
-        }, 
-        selectedElementTitle() {
-            return this.selectedElement?.attributes?.title;
-        },
-        selectedElementParent() {
-            let parentData = this.selectedElement?.relationships?.parent?.data;
-            if (parentData){
-                return this.structuralElementById({id: parentData.id});
-            }
-
-            return null;
-        },
-        selectedElementParentTitle() {
-            if (this.selectedElementParent) {
-                return this.selectedElementParent.attributes.title;
-            }
-
-            return '';
-        },
-        children() {
-            if (!this.selectedElement) {
-                return [];
-            }
-
-            return this.childrenById(this.selectedElement.id)
-                .map((id) => this.structuralElementById({ id }))
-                .filter(Boolean);
-        },
-    },
-    mounted() {
-        this.initWizardData();
-        this.updateUserUnits();
-        },
-    methods: {
-        ...mapActions({
-            showElementLinkDialog: 'showElementLinkDialog',
-            loadUserUnits: 'loadUserUnits',
-            loadStructuralElement: 'courseware-structural-elements/loadById',
-            linkStructuralElement: 'linkStructuralElement',
-            companionError: 'companionError',
-            companionSuccess: 'companionSuccess',
-        }),
-        initWizardData() {
-            this.selectedRange = '';
-            this.selectedUnit = null;
-            this.validateSelection();
-        },
-        async updateUserUnits() {
-            this.loadingUnits = true;
-            await this.loadUserUnits(this.userId);
-            this.loadingUnits = false;
-        },
-        getUnitElement(unit) {
-            return this.structuralElementById({id: unit.relationships['structural-element'].data.id});
-        },
-        linkElement() {
-            let view = this;
-            this.linkStructuralElement({
-                parentId: this.currentElement,
-                        elementId: this.selectedElement.id,
-                })
-                .then( () => {
-                    view.companionSuccess({
-                        info: view.$gettextInterpolate(
-                            view.$gettext('Die Seite %{ pageTitle } wurde erfolgreich verknüpft.'),
-                            { pageTitle: view.selectedElementTitle }
-                        )
-                    });
-                })
-                .catch( () => {
-                    view.companionError({
-                        info: view.$gettextInterpolate(
-                            view.$gettext('Die Seite %{ pageTitle } konnte nicht verknüpft werden.'),
-                            { pageTitle: view.selectedElementTitle }
-                        )
-                    });
-                })
-                .finally(() => {
-                    view.showElementLinkDialog(false);
-                });
-        },
-        selectElement(id) {
-            this.selectedElement = this.structuralElementById({id: id});
-            this.loadStructuralElement({id: id, options: {include: 'children'}});
-        },
-        validateSelection() {
-            this.requirements = [];
-            if (this.selectedUnit === null) {
-                this.requirements.push({slot: this.wizardSlots[0], text: this.$gettext('Lernmaterial') });
-            }
-            if (this.selectedElement === null) {
-                this.requirements.push({slot: this.wizardSlots[1], text: this.$gettext('Seite') });
-            }
-        }
-    },
-    watch: {
-        selectedElement(newElement) {
-            this.validateSelection();
-            if (newElement !== null) {
-                this.wizardSlots[1].valid = true;
-            } else {
-                this.wizardSlots[1].valid = false;
-            }
-        },
-        async selectedUnit(newUnit) {
-            this.validateSelection();
-            if (newUnit !== null) {
-                this.wizardSlots[0].valid = true;
-                await this.loadStructuralElement({id: this.selectedUnitRootId, options: {include: 'children'}});
-                this.selectedElement = null;
-            } else {
-                this.wizardSlots[0].valid = false;
-            }
-        },
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue b/resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue
deleted file mode 100644
index fcd275c..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-    <div class="cw-structural-element-discussion">
-        <courseware-collapsible-box
-            :title="text.comments"
-            :open="hasComments"
-        >
-            <courseware-structural-element-comments
-            :structuralElement="structuralElement"
-            @hasComments="hasComments = true"
-            />
-        </courseware-collapsible-box>
-
-        <courseware-collapsible-box
-            v-if="canEdit || userIsTeacher"
-            :title="text.feedback"
-            :open="hasFeedback"
-        >
-            <courseware-structural-element-feedback
-                :structuralElement="structuralElement"
-                :canEdit="canEdit"
-                @hasFeedback="hasFeedback = true"
-            />
-        </courseware-collapsible-box>
-    </div>
-</template>
-
-<script>
-import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
-import CoursewareStructuralElementComments from './CoursewareStructuralElementComments.vue';
-import CoursewareStructuralElementFeedback from './CoursewareStructuralElementFeedback.vue';
-import { mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-structural-element-discussion',
-    components: {
-        CoursewareCollapsibleBox,
-        CoursewareStructuralElementComments,
-        CoursewareStructuralElementFeedback,
-    },
-    props: {
-        structuralElement: Object,
-        canEdit: Boolean
-    },
-    data() {
-        return {
-            hasComments: false,
-            hasFeedback: false,
-            text: {
-                comments: this.$gettext('Kommentare zur Seite'),
-                feedback: this.$gettext('Feedback zur Seite')
-            }
-        }
-    },
-    computed: {
-        ...mapGetters({
-            userIsTeacher: 'userIsTeacher',
-        }),
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue b/resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue
deleted file mode 100644
index 3fa2c3c..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue
+++ /dev/null
@@ -1,141 +0,0 @@
-<template>
-    <section
-        v-if="canEdit || userIsTeacher"
-        class="cw-structural-element-feedback"
-        :class="[emptyFeedback ? 'cw-structural-element-feedback-empty' : '']"
-    >
-        <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
-        <div class="cw-structural-element-feedback-items" v-show="!emptyFeedback" ref="feedbacks">
-            <courseware-talk-bubble
-                v-for="feedback in feedback"
-                :key="feedback.id"
-                :payload="buildPayload(feedback)"
-            />
-        </div>
-        <courseware-companion-box
-                v-if="!userIsTeacher && feedback.length === 0"
-                :msgCompanion="$gettext('Es wurde noch kein Feedback abgegeben.')"
-                mood="pointing"
-            />
-        <div v-if="userIsTeacher" class="cw-structural-element-feedback-create">
-            <textarea v-model="feedbackText" :placeholder="placeHolder" spellcheck="true"></textarea>
-            <button class="button" @click="postFeedback"><translate>Senden</translate></button>
-        </div>
-    </section>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareTalkBubble from './CoursewareTalkBubble.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-structural-element-feedback',
-    components: {
-        CoursewareCompanionBox,
-        CoursewareTalkBubble,
-    },
-    props: {
-        structuralElement: Object,
-        canEdit: Boolean,
-    },
-    data() {
-        return {
-            feedbackText: '',
-            placeHolder: this.$gettext('Schreiben Sie ein Feedback...'),
-            srMessage: ''
-        };
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            getRelatedFeedback: 'courseware-structural-element-feedback/related',
-            getRelatedUser: 'users/related',
-            userIsTeacher: 'userIsTeacher',
-        }),
-        feedback() {
-            const parent = {
-                type: this.structuralElement.type,
-                id: this.structuralElement.id,
-            };
-
-            return this.getRelatedFeedback({ parent, relationship: 'feedback' });
-        },
-        emptyFeedback() {
-            if (this.feedback === null || this.feedback.length === 0) {
-                return true;
-            }
-
-            return false;
-        }
-    },
-    methods: {
-        ...mapActions({
-            createFeedback: 'courseware-structural-element-feedback/create',
-            loadRelatedFeedback: 'courseware-structural-element-feedback/loadRelated',
-        }),
-        buildPayload(feedback) {
-            const { id, type } = feedback;
-            const user = this.getRelatedUser({ parent: { id, type }, relationship: 'user' });
-
-            return {
-                own: user.id === this.userId,
-                content: feedback.attributes.feedback,
-                chdate: feedback.attributes.chdate,
-                mkdate: feedback.attributes.mkdate,
-                user_name: user?.attributes?.['formatted-name'] ?? '',
-                user_avatar: user?.meta?.avatar.small,
-            };
-        },
-        async loadFeedback() {
-            const parent = {
-                type: this.structuralElement.type,
-                id: this.structuralElement.id,
-            };
-            await this.loadRelatedFeedback({
-                parent,
-                relationship: 'feedback',
-                options: {
-                    include: 'user',
-                },
-            });
-        },
-        async postFeedback() {
-            this.updateSrMessage(this.$gettext('Feedback gesendet'));
-            const data = {
-                attributes: {
-                    feedback: this.feedbackText,
-                },
-                relationships: {
-                    'structural-element': {
-                        data: {
-                            id: this.structuralElement.id,
-                            type: this.structuralElement.type
-                        }
-                    }
-                },
-            };
-            await this.createFeedback( data, { root: true });
-            this.feedbackText = '';
-            this.loadFeedback();
-        },
-        updateSrMessage(message) {
-            this.srMessage = '';
-            this.srMessage = message;
-        }
-    },
-    async mounted() {
-        await this.loadFeedback();
-    },
-    updated() {
-        this.$refs.feedbacks.scrollTop = this.$refs.feedbacks.scrollHeight;
-    },
-    watch: {
-        feedback() {
-            if (this.feedback && this.feedback.length > 0) {
-                this.$emit('hasFeedback');
-            }
-        }
-    }
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue b/resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue
deleted file mode 100644
index 473b82f..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue
+++ /dev/null
@@ -1,471 +0,0 @@
-<template>
-    <div class="cw-element-permissions">
-        <label>
-            <input type="checkbox" class="default" v-model="userPermsReadAll" />
-            <translate>Alle Teilnehmenden haben Leserechte</translate>
-        </label>
-        <label>
-            <input type="checkbox" class="default" v-model="userPermsWriteAll" />
-            <translate>Alle Teilnehmenden haben Schreibrechte</translate>
-        </label>
-
-        <table class="default" v-if="autor_members.length">
-            <caption>
-                <translate>Studierende</translate>
-            </caption>
-            <colgroup>
-                <col style="width:1%" />
-                <col style="width:19%" />
-                <col style="width:1%" />
-                <col style="width:29%" />
-                <col style="width:50%" />
-            </colgroup>
-            <thead>
-                <tr>
-                    <th><input type="checkbox" v-model="bulkSelectAutorRead" @click="handleBulkSelectRead($event, 'autor')"/></th>
-                    <th><translate>Lesen</translate></th>
-                    <th><input type="checkbox" v-model="bulkSelectAutorWrite" @click="handleBulkSelectWrite($event)"/></th>
-                    <th><translate>Lesen und Schreiben</translate></th>
-                    <th><translate>Name</translate></th>
-                </tr>
-            </thead>
-            <tbody>
-                <tr v-for="user in autor_members_filtered" :key="user.user_id">
-                    <td class="perm" colspan="2">
-                        <input
-                            type="checkbox"
-                            :id="user.user_id + `_read`"
-                            :value="user.user_id"
-                            v-model="userPermsReadUsers"
-                        />
-                    </td>
-                    <td class="perm" colspan="2">
-                        <input
-                            type="checkbox"
-                            :id="user.user_id + `_write`"
-                            :value="user.user_id"
-                            v-model="userPermsWriteUsers"
-                        />
-                    </td>
-
-                    <td>
-                        <label :for="user.user_id + `_read`">
-                            {{ user.formattedname }}
-                            <i>{{ user.username }}</i>
-                        </label>
-                    </td>
-                </tr>
-            </tbody>
-            <tfoot v-if="can_paginate && autor_members.length > entries_per_page">
-                <tr>
-                    <td colspan="5">
-                        <studip-pagination
-                            :currentOffset="autorOffset"
-                            :totalItems="autor_members.length"
-                            :itemsPerPage="entries_per_page"
-                            @updateOffset="updateAutorOffset" />
-                    </td>
-                </tr>
-            </tfoot>
-        </table>
-
-        <table class="default" v-if="user_members.length">
-            <caption>
-                <translate>Leser/-innen</translate>
-            </caption>
-            <colgroup>
-                <col style="width:1%" />
-                <col style="width:39%" />
-                <col style="width:50%" />
-            </colgroup>
-            <thead>
-                <tr>
-                    <th><input type="checkbox" v-model="bulkSelectUserRead" @click="handleBulkSelectRead($event, 'user')"/></th>
-                    <th><translate>Lesen</translate></th>
-                    <th><translate>Name</translate></th>
-                </tr>
-            </thead>
-            <tbody>
-                <tr v-for="user in user_members_filtered" :key="user.user_id">
-                    <td colspan="2">
-                        <input
-                            type="checkbox"
-                            :id="user.user_id + `_read`"
-                            :value="user.user_id"
-                            v-model="userPermsReadUsers"
-                        />
-                    </td>
-                    <td>
-                        <label :for="user.user_id + `_read`">
-                            {{ user.firstname }}
-                            {{ user.lastname }}
-                            <i>{{ user.username }}</i>
-                        </label>
-                    </td>
-                </tr>
-            </tbody>
-            <tfoot v-if="can_paginate && user_members.length > entries_per_page">
-                <tr>
-                    <td colspan="3">
-                        <studip-pagination
-                            :currentOffset="userOffset"
-                            :totalItems="user_members.length"
-                            :itemsPerPage="entries_per_page"
-                            @updateOffset="updateUserOffset"/>
-                    </td>
-                </tr>
-            </tfoot>
-        </table>
-
-        <table class="default" v-if="groups.length">
-            <caption>
-                <translate>Gruppen</translate>
-            </caption>
-            <colgroup>
-                <col style="width:20%" />
-                <col style="width:35%" />
-                <col style="width:45%" />
-            </colgroup>
-            <thead>
-                <tr>
-                    <th><translate>Lesen</translate></th>
-                    <th><translate>Lesen und Schreiben</translate></th>
-                    <th><translate>Name</translate></th>
-                </tr>
-            </thead>
-            <tbody>
-                <tr v-for="group in groups" :key="group.id">
-                    <td class="perm">
-                        <input
-                            type="checkbox"
-                            :id="group.id + `_read`"
-                            :value="group.id"
-                            v-model="userPermsReadGroups"
-                        />
-                    </td>
-                    <td class="perm">
-                        <input
-                            type="checkbox"
-                            :id="group.id + `_write`"
-                            :value="group.id"
-                            v-model="userPermsWriteGroups"
-                        />
-                    </td>
-
-                    <td>
-                        <label :for="group.id + `_read`">
-                            {{ group.name }}
-                        </label>
-                    </td>
-                </tr>
-            </tbody>
-        </table>
-    </div>
-</template>
-<script>
-import StudipPagination from './../StudipPagination.vue';
-
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-structural-element-permissions',
-    props: {
-        element: Object,
-    },
-    components: {
-        StudipPagination
-    },
-    data() {
-        return {
-            user_perms: {},
-            userPermsReadUsers: [],
-            userPermsReadGroups: [],
-            userPermsReadAll: Boolean,
-            userPermsWriteUsers: [],
-            userPermsWriteGroups: [],
-            userPermsWriteAll: Boolean,
-            bulkSelectAutorRead: false,
-            bulkSelectUserRead: false,
-            bulkSelectAutorWrite: false,
-            userOffset: 0,
-            autorOffset: 0,
-        };
-    },
-
-    mounted() {
-        if (this.element.attributes['read-approval'].users !== undefined) {
-            this.userPermsReadUsers = this.element.attributes['read-approval'].users;
-        }
-        if (this.element.attributes['read-approval'].groups !== undefined) {
-            this.userPermsReadGroups = this.element.attributes['read-approval'].groups;
-        }
-        if (this.element.attributes['read-approval'].all !== undefined) {
-            this.userPermsReadAll = this.element.attributes['read-approval'].all;
-        } else {
-            this.userPermsReadAll = true;
-        }
-        if (this.element.attributes['write-approval'].users !== undefined) {
-            this.userPermsWriteUsers = this.element.attributes['write-approval'].users;
-        }
-        if (this.element.attributes['write-approval'].groups !== undefined) {
-            this.userPermsWriteGroups = this.element.attributes['write-approval'].groups;
-        }
-        if (this.element.attributes['write-approval'].all !== undefined) {
-            this.userPermsWriteAll = this.element.attributes['write-approval'].all;
-        } else {
-            this.userPermsWriteAll = false;
-        }
-
-        // load memberships for coursewares in a course context
-        if (this.context.type === 'courses') {
-            const parent = { type: 'courses', id: this.context.id };
-            let options = {
-                include: 'user',
-                'page[limit]': 10000,
-            }
-            this.loadCourseMemberships({ parent, relationship: 'memberships', options: options });
-            this.loadCourseStatusGroups({ parent, relationship: 'status-groups' });
-        }
-    },
-
-    updated () {
-        this.handleBulkSelectReadPassive('autor');
-        this.handleBulkSelectReadPassive('user');
-        this.handleBulkSelectWritePassive();
-    },
-
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            courseware: 'courseware',
-            course: 'courses/related',
-            relatedCourseMemberships: 'course-memberships/related',
-            relatedCourseStatusGroups: 'status-groups/related',
-            relatedUser: 'users/related',
-        }),
-        users() {
-            const parent = { type: 'courses', id: this.context.id };
-            const relationship = 'memberships';
-            const memberships = this.relatedCourseMemberships({ parent, relationship });
-
-            return (
-                memberships?.map((membership) => {
-                    const parent = { type: membership.type, id: membership.id };
-                    const member = this.relatedUser({ parent, relationship: 'user' });
-
-                    return {
-                        user_id: member.id,
-                        formattedname: member.attributes['formatted-name'],
-                        username: member.attributes['username'],
-                        perm: membership.attributes['permission'],
-                    };
-                }) ?? []
-            );
-        },
-        groups() {
-            const parent = { type: 'courses', id: this.context.id };
-            const relationship = 'status-groups';
-            const statusGroups = this.relatedCourseStatusGroups({ parent, relationship });
-
-            return (
-                statusGroups?.map((statusGroup) => {
-                    return {
-                        id: statusGroup.id,
-                        name: statusGroup.attributes['name'],
-                    };
-                }) ?? []
-            );
-        },
-        autor_members() {
-            if (Object.keys(this.users).length === 0 && this.users.constructor === Object) {
-                return [];
-            }
-
-            let members = this.users.filter(function (user) {
-                return user.perm === 'autor';
-            });
-
-            return members;
-        },
-
-        autor_members_filtered() {
-            if (this.autor_members.length === 0) {
-                return [];
-            }
-            let start = this.autorOffset * this.entries_per_page;
-            let end = ((this.autorOffset + 1) * this.entries_per_page);
-            return this.autor_members.slice(start, end);
-        },
-
-        user_members() {
-            if (Object.keys(this.users).length === 0 && this.users.constructor === Object) {
-                return [];
-            }
-
-            let members = this.users.filter(function (user) {
-                return user.perm === 'user';
-            });
-
-            return members;
-        },
-
-        user_members_filtered() {
-            if (this.user_members.length === 0) {
-                return [];
-            }
-            let start = this.userOffset * this.entries_per_page;
-            let end = ((this.userOffset + 1) * this.entries_per_page);
-            return this.user_members.slice(start, end);
-        },
-
-        entries_per_page() {
-            return STUDIP?.config?.ENTRIES_PER_PAGE ?? 0;
-        },
-
-        can_paginate() {
-            return this.entries_per_page > 0;
-        },
-
-        readApproval() {
-            return {
-                all: this.userPermsReadAll,
-                users: this.userPermsReadUsers,
-                groups: this.userPermsReadGroups,
-            };
-        },
-
-        writeApproval() {
-            return {
-                all: this.userPermsWriteAll,
-                users: this.userPermsWriteUsers,
-                groups: this.userPermsWriteGroups,
-            };
-        },
-    },
-
-    methods: {
-        ...mapActions({
-            loadCourseMemberships: 'course-memberships/loadRelated',
-            loadCourseStatusGroups: 'status-groups/loadRelated',
-        }),
-
-        updateAutorOffset(offset) {
-            this.autorOffset = parseInt(offset);
-        },
-
-        updateUserOffset(offset) {
-            this.userOffset = parseInt(offset);
-        },
-
-        handleBulkSelectRead(event, type) {
-            let state = event.target.checked;
-            let list = type === 'autor' ? this.autor_members_filtered : this.user_members_filtered;
-            if (list.length == 0) {
-                return;
-            }
-            if (type === 'autor') {
-                this.bulkSelectAutorRead = state;
-            } else {
-                this.bulkSelectUserRead = state;
-            }
-            for (let user of list) {
-                if (state) { // Add
-                    if (this.userPermsReadUsers.includes(user.user_id) === false) {
-                        this.userPermsReadUsers.push(user.user_id);
-                    }
-                } else { // Remove
-                    if (this.userPermsReadUsers.includes(user.user_id) === true) {
-                        let index = this.userPermsReadUsers.findIndex((perm) => perm == user.user_id);
-                        this.userPermsReadUsers.splice(index, 1);
-                    }
-                }
-            }
-        },
-
-        handleBulkSelectWrite(event) {
-            let state = event.target.checked;
-            let list = this.autor_members_filtered;
-            if (list.length == 0) {
-                return;
-            }
-            this.bulkSelectAutorWrite = state;
-            for (let user of list) {
-                if (state) { // Add
-                    if (this.userPermsWriteUsers.includes(user.user_id) === false) {
-                        this.userPermsWriteUsers.push(user.user_id);
-                    }
-                } else { // Remove
-                    if (this.userPermsWriteUsers.includes(user.user_id) === true) {
-                        let index = this.userPermsWriteUsers.findIndex((perm) => perm == user.user_id);
-                        this.userPermsWriteUsers.splice(index, 1);
-                    }
-                }
-            }
-        },
-
-        handleBulkSelectReadPassive(type) {
-            let bulkState = false;
-            if (type === 'autor' && this.autor_members_filtered?.length > 0) {
-                let currentAutorsIds = this.autor_members_filtered.map((autor) => autor.user_id);
-                if (currentAutorsIds.every((id) => this.userPermsReadUsers.includes(id))) {
-                    bulkState = true;
-                }
-                this.bulkSelectAutorRead = bulkState;
-            }
-            if (type === 'user' && this.user_members_filtered?.length > 0) {
-                let currentUsersIds = this.user_members_filtered.map((user) => user.user_id);
-                if (currentUsersIds.every((id) => this.userPermsReadUsers.includes(id))) {
-                    bulkState = true;
-                }
-                this.bulkSelectUserRead = bulkState;
-            }
-        },
-
-        handleBulkSelectWritePassive() {
-            let bulkState = false;
-            let currentAutorsIds = this.autor_members_filtered.map((autor) => autor.user_id);
-            if (currentAutorsIds.every((id) => this.userPermsWriteUsers.includes(id))) {
-                bulkState = true;
-            }
-            this.bulkSelectAutorWrite = bulkState;
-        }
-    },
-
-    watch: {
-        userPermsReadUsers(newVal, oldVal) {
-            this.handleBulkSelectReadPassive('autor');
-            this.handleBulkSelectReadPassive('user');
-            this.$emit('updateReadApproval', this.readApproval);
-        },
-        userPermsReadGroups(newVal, oldVal) {
-            this.$emit('updateReadApproval', this.readApproval);
-        },
-        userPermsReadAll(newVal, oldVal) {
-            this.$emit('updateReadApproval', this.readApproval);
-            if (newVal === true) {
-                this.userPermsWriteAll = false;
-            }
-        },
-        userPermsWriteUsers(newVal, oldVal) {
-            this.handleBulkSelectWritePassive();
-            this.$emit('updateWriteApproval', this.writeApproval);
-        },
-        userPermsWriteGroups(newVal, oldVal) {
-            this.$emit('updateWriteApproval', this.writeApproval);
-        },
-        userPermsWriteAll(newVal, oldVal) {
-            this.$emit('updateWriteApproval', this.writeApproval);
-            if (newVal === true) {
-                this.userPermsReadAll = false;
-            }
-        },
-        autorOffset(newVal, oldVal) {
-            this.handleBulkSelectReadPassive('autor');
-            this.handleBulkSelectWritePassive();
-        },
-        userOffset(newVal, oldVal) {
-            this.handleBulkSelectReadPassive('user');
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementSelector.vue b/resources/vue/components/courseware/CoursewareStructuralElementSelector.vue
deleted file mode 100644
index 0e8acda..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElementSelector.vue
+++ /dev/null
@@ -1,141 +0,0 @@
-<template>
-    <ul class="courseware-structural-element-selector" role="radiogroup">
-        <courseware-structural-element-selector-item 
-            v-if="rootElement !== null"
-            :element="rootElement"
-            :siblings="[]"
-            :selectedId="selectedId"
-            :focusedElementId="focusedElementId"
-            :rootId="rootId"
-            :validateAncestors="validateAncestors"
-            :targetId="targetId"
-            :targetAncestors="targetAncestors"
-            :selectablePurposes="selectablePurposes"
-            @input="handleInput"
-            @focus="handleFocus"
-        />
-    </ul>
-</template>
-
-<script>
-import CoursewareStructuralElementSelectorItem from './CoursewareStructuralElementSelectorItem.vue';
-import { mapActions, mapGetters } from 'vuex'
-
-export default {
-    name: 'courseware-structural-element-selector',
-    components: {
-        CoursewareStructuralElementSelectorItem
-    },
-    model: {
-        prop: 'element'
-    },
-    props: {
-        element: {
-            type: Object
-        },
-        rootId: {
-            type: String,
-            required: true
-        },  
-        validateAncestors: {
-            type: Boolean,
-            default: false
-        },
-        targetId: {
-            type: String,
-            default: null
-        },
-        selectablePurposes: {
-            type: Array,
-            default: () => []
-        }
-    },
-    data() {
-        return {
-            rootElement: null,
-            focusedElementId: '' 
-        };
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            coursewareUnits: 'courseware-units/all',
-            structuralElementById: 'courseware-structural-elements/byId',
-            context: 'context',
-            childrenById: 'courseware-structure/children',
-            currentElement: 'currentElement'
-        }),
-        children() {
-            if (!this.rootElement) {
-                return [];
-            }
-
-            return this.childrenById(this.rootElement.id)
-                .map((id) => this.structuralElementById({ id }))
-                .filter(Boolean);
-        },
-        selectedId() {
-            return this.element?.id ?? '';
-        },
-        targetElement() {
-            return this.structuralElementById({ id: this.targetId });
-        },
-        targetAncestors() {
-            if (!this.targetElement || !this.validateAncestors) {
-                return [];
-            }
-
-            const finder = (parent) => {
-                const parentId = parent.relationships?.parent?.data?.id;
-                if (!parentId) {
-                    return null;
-                }
-                const element = this.structuralElementById({ id: parentId });
-                if (!element) {
-                    console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`);
-                }
-
-                return element;
-            };
-
-            const visitAncestors = function* (node) {
-                const parent = finder(node);
-                if (parent) {
-                    yield parent;
-                    yield* visitAncestors(parent);
-                }
-            };
-
-            return [...visitAncestors(this.targetElement)].reverse();
-        },
-    },
-    methods: {
-        ...mapActions({
-            loadStructuralElement: 'courseware-structural-elements/loadById',
-            companionError: 'companionError',
-            companionSuccess: 'companionSuccess',
-        }),
-        handleInput(id) {
-            this.$emit('input', this.structuralElementById({ id }));
-            this.focusedElementId = id;
-        },
-        handleFocus(id) {
-            this.focusedElementId = id;
-        },
-        async loadRootElement(rootId) {
-            this.rootElement = null;
-            await this.loadStructuralElement({id: rootId, options: {include: 'children'}});
-            this.rootElement = this.structuralElementById({ id: rootId});
-        }
-    },
-    async mounted() {
-        await this.loadRootElement(this.rootId);
-    },
-    watch: {
-        async rootId(newRootId) {
-            await this.loadRootElement(newRootId);
-            this.focusedElementId = '';
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementSelectorItem.vue b/resources/vue/components/courseware/CoursewareStructuralElementSelectorItem.vue
deleted file mode 100644
index f6b3cf6..0000000
--- a/resources/vue/components/courseware/CoursewareStructuralElementSelectorItem.vue
+++ /dev/null
@@ -1,272 +0,0 @@
-<template>
-    <li class="courseware-structural-element-selector-item">
-        <span
-            class="radiobutton"
-            :tabindex="tabindex"
-            :ref="'radiobutton-' + element.id"
-            role="radio"
-            :aria-checked="selected ? 'true' : 'false'"
-            :aria-labelledby="labelId"
-            @click="handleClickInput(element.id)"
-            @keydown="handleKeyInput($event, element.id)"
-        >
-            <template v-if="selectable">
-                <studip-icon v-if="selected" shape="radiobutton-checked" />
-                <studip-icon v-else shape="radiobutton-unchecked" />
-            </template>
-            <studip-icon v-else shape="decline" role="inactive" />    
-        </span>
-        <template v-if="hasChildren">
-            <a href="#" :aria-expanded="isOpen ? 'true' : 'false'" @click.prevent="toggleChildrenVisibility">
-                <studip-icon v-if="!isOpen" shape="arr_1right" />
-                <studip-icon v-if="isOpen" shape="arr_1down" />
-                <label :id="labelId">
-                    {{ element.attributes.title }}
-                    <span v-if="!selectable" class="sr-only">{{ $gettext('nicht wählbar') }}</span>
-                </label>
-            </a>
-            <ul v-if="isOpen">
-                <courseware-structural-element-selector-item
-                    v-for="child in children"
-                    :key="child.id"
-                    :element="child"
-                    :siblings="children"
-                    :selectedId="selectedId"
-                    :focusedElementId="focusedElementId"
-                    :rootId="rootId"
-                    :validateAncestors="validateAncestors"
-                    :targetId="targetId"
-                    :targetAncestors="targetAncestors"
-                    :selectablePurposes="selectablePurposes"
-                    @input="handleInput"
-                    @focus="handleFocus"
-                    @selectable="updateSelectable"
-                />
-            </ul>
-        </template>
-        <template v-else>
-            <studip-icon shape="arr_1right" role="inactive" class="inactive"/>
-            <label :id="labelId">
-                {{ element.attributes.title }}
-                <span v-if="!selectable" class="sr-only">{{ $gettext('nicht wählbar') }}</span>
-            </label>
-        </template>
-    </li>
-</template>
-<script>
-import { mapActions, mapGetters } from 'vuex'
-
-export default {
-    name: 'courseware-structural-element-selector-item',
-    props: {
-        element: {
-            type: Object
-        },
-        siblings: {
-            type: Array
-        },
-        selectedId: {
-            type: String,
-            required: true
-        },
-        focusedElementId: {
-            type: String
-        },
-        rootId: {
-            type: String,
-            required: true
-        },  
-        validateAncestors: {
-            type: Boolean,
-            default: false
-        },
-        targetId: {
-            type: String,
-            default: null
-        },
-        targetAncestors: {
-            type: Array
-        },
-        selectablePurposes: {
-            type: Array
-        }
-    },
-    data() {
-        return {
-            isOpen: false,
-            selectable: true,
-        }
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId',
-            coursewareUnits: 'courseware-units/all',
-            structuralElementById: 'courseware-structural-elements/byId',
-            context: 'context',
-            currentElement: 'currentElement'
-        }),
-        children() {
-            const children = this.element?.relationships?.children?.data?.map(child => child.id);
-            if (!children) {
-                return [];
-            }
-
-            return children.map((id) => this.structuralElementById({ id })).filter(Boolean);
-        },
-        hasChildren() {
-            return this.children.length > 0;
-        },
-        selected() {
-            return this.selectedId === this.element?.id;
-        },
-        focused() {
-            return this.focusedElementId === this.element?.id;
-        },
-        labelId() {
-            return this.element.id + '_checkbox-label';
-        },
-        isRoot() {
-            return this.rootId === this.element.id;
-        },
-        tabindex() {
-            if (this.focusedElementId !== '') {
-                return this.focused ? 0 : -1;
-            }
-            return this.isRoot ? 0 : -1;
-        },
-        nextElementId() {
-            if (this.hasChildren && this.isOpen) {
-                return this.children[0].id;
-            }
-
-            return this.nextSiblingId;
-        },
-        nextSiblingId() {
-            if (this.isRoot) {
-                return null;
-            }
-            const index = this.siblings.findIndex(element => element.id === this.element.id) + 1;
-            if (this.siblings.length > index) {
-                return this.siblings[index].id;
-            } else {
-                return this.$parent.nextSiblingId;
-            }
-        },
-        previousElementId() {
-            if (this.isRoot) {
-                return null;
-            }
-            const index = this.siblings.findIndex(element => element.id === this.element.id) - 1;
-            if (index > -1) {
-                const childrenCount = this.siblings[index].relationships.children.data.length;
-                const previousElement = this.$parent.$children.find(child => child.element?.id === this.siblings[index].id);
-                if (childrenCount > 0 && previousElement.isOpen) {
-                        const element = this.structuralElementById({ id: this.siblings[index].relationships.children.data[childrenCount - 1].id });
-                        if (
-                            element.relationships.children.data.length > 0
-                            && previousElement.$children.find(child => child.element?.id === element.id).isOpen
-                        ) {
-                            return element.relationships.children.data[element.relationships.children.data.length -1].id;
-                        }
-                        return element.id;
-                } else {
-                    return this.siblings[index].id;
-                } 
-            } else {
-                return this.$parent.element.id;
-            }
-        },
-    },
-    methods: {
-        ...mapActions({
-            loadStructuralElement: 'courseware-structural-elements/loadById',
-            companionError: 'companionError',
-            companionSuccess: 'companionSuccess',
-        }),
-        loadChildren() {
-            const children = this.element?.relationships?.children?.data?.map(child => child.id) ?? [];
-            children.forEach((id) => this.loadStructuralElement({id: id,  options: {include: 'children'}}));
-        },
-        toggleChildrenVisibility() {
-            if (!this.isOpen) {
-                this.loadChildren();
-            }
-            this.isOpen = !this.isOpen;
-        },
-        handleInput(id) {
-            this.$emit('input', id);
-        },
-        handleFocus(id) {
-            this.$emit('focus', id);
-        },
-        validate() {
-            if (
-                this.element.id === this.targetId
-                || this.targetAncestors.find(ancestor => ancestor.id === this.element.id)
-            ) {
-                this.selectable = false;
-                this.$emit('selectable', false);
-            }
-        },
-        updateSelectable() {
-            this.selectable = false;
-            this.$emit('selectable', false);
-        },
-        filterSelectablePurposes() {
-            if (this.selectablePurposes.length === 0) {
-                return;
-            }
-            this.selectable = this.selectablePurposes.includes(this.element.attributes.purpose);
-        },
-        handleClickInput(id) {
-            if (this.selectable) {
-                this.handleInput(id);
-            }
-        },
-        handleKeyInput(event, id) {
-            switch(event.keyCode) {
-                case 37: // arrow left
-                case 38: // arrow up
-                    event.preventDefault();
-                    if (this.previousElementId !== null) {
-                        this.$emit('focus', this.previousElementId);    
-                    }
-                    break;
-                case 39: // arrow right
-                case 40: // arrow down
-                    event.preventDefault();
-                    if (this.nextElementId !== null) {
-                        this.$emit('focus', this.nextElementId);
-                    }
-                    break;
-            }
-        },
-        selectRoot() {
-            if (this.focusedElementId === '' && this.isRoot && this.selectable) {
-                this.handleInput(this.element.id);
-            }
-        }
-    },
-    mounted() {
-        this.loadChildren();
-        if (this.validateAncestors) {
-            this.validate();
-        }
-        this.filterSelectablePurposes();
-        this.selectRoot();
-    },
-    watch: {
-        focusedElementId(newId) {
-            if (this.focused) {
-                this.$refs['radiobutton-'+ this.element.id].focus();
-                if (this.selectable) {
-                    this.handleInput(newId);
-                } else {
-                    this.handleInput('');
-                }
-            }
-            this.selectRoot();
-        }
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTab.vue b/resources/vue/components/courseware/CoursewareTab.vue
deleted file mode 100644
index 191f552..0000000
--- a/resources/vue/components/courseware/CoursewareTab.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<template>
-    <div
-        role="tabpanel"
-        class="cw-tab"
-        :id="id"
-        :class="{ 'cw-tab-active': isActive }"
-        :aria-labelledby="selectorId"
-    >
-        <slot></slot>
-    </div>
-</template>
-
-<script>
-export default {
-    name: 'courseware-tab',
-    props: {
-        name: {type: String, required: true },
-        alias: {type: String, default: ''},
-        selected: { type: Boolean, default: false },
-        index: {type: Number, required: true },
-        icon: {type: String, default: ''},
-    },
-    data() {
-        return {
-            isActive: false,
-        };
-    },
-    computed: {
-        selectorId() {
-            return '#' + this._uid + '-' + this.name.toLowerCase().replace(/ /g, '-');
-        },
-        id() {
-            return this._uid + '-' + this.name.toLowerCase().replace(/ /g, '-') + '-tabpanel';
-        }
-    },
-    mounted() {
-        this.isActive = this.selected;
-    },
-    updated () {
-        if (this.isActive) {
-            STUDIP.eventBus.emit('courseware:update-tab',{ 'uid': this._uid });
-        }
-
-    },
-    watch: {
-        selected(newValue) {
-            this.isActive = newValue;
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
deleted file mode 100644
index e495323..0000000
--- a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
+++ /dev/null
@@ -1,234 +0,0 @@
-<template>
-    <div class="cw-block cw-block-table-of-contents">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeText"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="childElementsWithTasks.length > 0">
-                    <div v-if="currentStyle !== 'tiles' && currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
-                    <ul
-                        v-if="currentStyle === 'list-details' || currentStyle === 'list'"
-                        :class="['cw-block-table-of-contents-' + currentStyle]"
-                    >
-                        <li v-for="child in childElementsWithTasks" :key="child.id">
-                            <router-link :to="'/structural_element/' + child.id">
-                                <div class="cw-block-table-of-contents-title-box" :class="[child.attributes.payload.color]">
-                                    {{ child.attributes.title }}
-                                    <span v-if="child.attributes.purpose === 'task'"> | {{ child.solverName }}</span>
-                                    <p v-if="currentStyle === 'list-details'">{{ child.attributes.payload.description }}</p>
-                                </div>
-                            </router-link>
-                        </li>
-                    </ul>
-                    <ul
-                        v-if="currentStyle === 'tiles'" 
-                        class="cw-block-table-of-contents-tiles cw-tiles"
-                    >
-                        <li
-                            v-for="child in childElementsWithTasks"
-                            :key="child.id"
-                            class="tile"
-                            :class="[child.attributes.payload.color]"
-                        >
-                            <router-link :to="'/structural_element/' + child.id" :title="child.attributes.purpose === 'task' ? child.attributes.title + ' | ' + child.solverName : child.attributes.title"> 
-                                <div
-                                    class="preview-image"
-                                    :class="[hasImage(child) ? '' : 'default-image']"
-                                    :style="getChildStyle(child)"
-                                >
-                                    <div v-if="child.attributes.purpose === 'task'" class="overlay-text">{{ child.solverName }}</div>
-                                </div>
-                                <div class="description">
-                                    <header
-                                        :class="[child.attributes.purpose !== '' ? 'description-icon-' + child.attributes.purpose : '']"
-                                    >
-                                        {{ child.attributes.title || "–"}}
-                                    </header>
-                                    <div class="description-text-wrapper">
-                                        <p>{{ child.attributes.payload.description }}</p>
-                                    </div>
-                                    <footer>
-                                        {{ countChildChildren(child) }}
-                                        <translate
-                                            :translate-n="countChildChildren(child)"
-                                            translate-plural="Seiten"
-                                        >
-                                        Seite
-                                        </translate>
-                                    </footer>
-                                </div>
-                            </router-link>
-                        </li>
-                    </ul>
-                </div>
-                <courseware-companion-box
-                    v-if="viewMode === 'edit' && childElementsWithTasks.length === 0"
-                    :msgCompanion="$gettext('Es sind noch keine Unterseiten vorhanden. ' +
-                        'Sobald Sie weitere Unterseiten anlegen, erscheinen diese automatisch hier im Inhaltsverzeichnis.')"
-                    mood="pointing"
-                />
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label>
-                        {{ $gettext('Überschrift') }}
-                        <input type="text" v-model="currentTitle" />
-                    </label>
-                    <label>
-                        {{ $gettext('Layout') }}
-                        <select v-model="currentStyle">
-                            <option value="list">{{ $gettext('Liste') }}</option>
-                            <option value="list-details">{{ $gettext('Liste mit Beschreibung') }}</option>
-                            <option value="tiles">{{ $gettext('Kacheln') }}</option>
-                        </select>
-                    </label>
-                </form>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Inhaltsverzeichnis-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-table-of-contents-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareCompanionBox,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentTitle: '',
-            currentStyle: '',
-        };
-    },
-    computed: {
-        ...mapGetters({
-            childrenById: 'courseware-structure/children',
-            structuralElementById: 'courseware-structural-elements/byId',
-            context: 'context',
-            taskById: 'courseware-tasks/byId',
-            userById: 'users/byId',
-            groupById: 'status-groups/byId',
-            viewMode: 'viewMode',
-        }),
-        structuralElement() {
-            return this.structuralElementById({ id: this.$route.params.id });
-        },
-        childElements() {
-            return this.childrenById(this.structuralElement.id).map((id) => this.structuralElementById({ id }));
-        },
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        style() {
-            return this.block?.attributes?.payload?.style;
-        },
-        childElementsWithTasks() {
-            let children = [];
-            this.childElements.forEach(element => {
-                if (element.relationships.task.data) {
-                    let solverName = this.getSolverName(element.relationships.task.data.id);
-                    if (solverName) {
-                        element.solverName = solverName;
-                        children.push(element);
-                    }
-                } else {
-                    children.push(element);
-                }
-            });
-
-            return children;
-        }
-    },
-    mounted() {
-        this.initCurrentData();
-        this.childElements.forEach(element => {
-            if (element.relationships.task.data) {
-                const taskId = element.relationships.task.data.id;
-                try {
-                    this.loadTask({
-                        taskId: taskId,
-                    });
-                } catch(error) {
-                    // nothing to do here
-                }
-            }
-        });
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            loadTask: 'loadTask',
-        }),
-        initCurrentData() {
-            this.currentTitle = this.title;
-            this.currentStyle = this.style;
-        },
-        storeText() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.title = this.currentTitle;
-            attributes.payload.style = this.currentStyle;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-        getChildStyle(child) {
-            let url = child.relationships?.image?.meta?.['download-url'];
-
-            if(url) {
-                return {'background-image': 'url(' + url + ')'};
-            } else {
-                return {};
-            }
-        },
-        countChildChildren(child) {
-            return this.childrenById(child.id).length + 1;
-        },
-        hasImage(child) {
-            return child.relationships?.image?.data !== null;
-        },
-
-        getSolverName(taskId) {
-            const task = this.taskById({ id: taskId});
-            if (task === undefined) {
-                return false;
-            }
-            const solver = task.relationships.solver.data;
-            if (solver.type === 'users') {
-                const user = this.userById({ id: solver.id });
-
-                return user.attributes['formatted-name'];
-            }
-            if (solver.type === 'status-groups') {
-                const group = this.groupById({ id: solver.id });
-
-                return group.attributes.name;
-            }
-
-            return false;
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTabs.vue b/resources/vue/components/courseware/CoursewareTabs.vue
deleted file mode 100644
index f38ae65..0000000
--- a/resources/vue/components/courseware/CoursewareTabs.vue
+++ /dev/null
@@ -1,133 +0,0 @@
-<template>
-    <div class="cw-tabs">
-        <div role="tablist" class="cw-tabs-nav">
-            <button
-                v-for="(tab, index) in tabs"
-                :key="index"
-                :class="[
-                    tab.isActive ? 'is-active' : '',
-                    tab.icon !== '' && tab.name !== '' ? 'cw-tabs-nav-icon-text-' + tab.icon : '',
-                    tab.icon !== '' && tab.name === '' ? 'cw-tabs-nav-icon-solo-' + tab.icon : '',
-                ]"
-                :aria-selected="tab.isActive"
-                :aria-controls="tab.id"
-                :id="tab.selectorId"
-                :tabindex="tab.isActive ? 0 : -1"
-                @click="selectTab(tab.selectorId)"
-                @keydown="handleKeyEvent($event)"
-                :ref="tab.selectorId"
-            >
-                {{ tab.name }}
-            </button>
-        </div>
-        <div class="cw-tabs-content">
-            <slot></slot>
-        </div>
-    </div>
-</template>
-
-<script>
-export default {
-    name: 'courseware-tabs',
-    props: {
-        setSelected: { type: Number, required: false, default: 0 },
-    },
-    data() {
-        return {
-            tabs: [],
-        };
-    },
-    created() {
-        this.tabs = this.$children.sort((a, b) => {
-            return a.index - b.index;
-        });
-    },
-    methods: {
-        selectTab(selectorId) {
-            let view = this;
-            this.tabs.forEach((tab) => {
-                tab.isActive = false;
-                if (tab.selectorId === selectorId) {
-                    tab.isActive = true;
-                    view.$refs[selectorId][0].focus();
-                    view.$emit('selectTab', tab);
-                }
-            });
-        },
-        selectTabByIndex(selectedIndex) {
-            let view = this;
-            this.tabs.forEach((tab) => {
-                tab.isActive = false;
-                if (tab.index === selectedIndex) {
-                    tab.isActive = true;
-                    view.$refs[tab.selectorId][0].focus();
-                    view.$emit('selectTab', tab);
-                }
-            });
-        },
-        handleKeyEvent(e) {
-            let tablist = e.target.parentElement;
-            switch (e.keyCode) {
-                case 37: // left
-                case 38: // up
-                    if (tablist.firstChild.id !== e.target.id) {
-                        this.selectTab(e.target.previousElementSibling.id);
-                    } else {
-                        this.selectTab(tablist.lastChild.id);
-                    }
-                    break;
-                case 39: // right
-                case 40: // down
-                    if (tablist.lastChild.id !== e.target.id) {
-                        this.selectTab(e.target.nextElementSibling.id);
-                    } else {
-                        this.selectTab(tablist.firstChild.id);
-                    }
-                    break;
-                case 36: //pos1
-                    this.selectTab(tablist.firstChild.id);
-                    break;
-                case 35: //end
-                    this.selectTab(tablist.lastChild.id);
-                    break;
-            }
-        },
-        getButtonById(id) {
-            return this.$refs[id][0];
-        },
-        getFirstTabButton() {
-            let selectorId = this.tabs[0].selectorId;
-            return this.$refs[selectorId][0];
-        },
-        getTabButtonByName(name) {
-            let selectorId = null;
-            this.tabs.forEach((tab) => {
-                if (tab.name === name) {
-                    selectorId = tab.selectorId;
-                }
-            });
-            if (selectorId) {
-                return this.$refs[selectorId][0];
-            }
-            return null;
-        },
-        getTabButtonByAlias(alias) {
-            let selectorId = null;
-            this.tabs.forEach((tab) => {
-                if (tab.alias === alias) {
-                    selectorId = tab.selectorId;
-                }
-            });
-            if (selectorId) {
-                return this.$refs[selectorId][0];
-            }
-            return null;
-        },
-    },
-    watch: {
-        setSelected(tab) {
-            this.selectTabByIndex(tab);
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTabsContainer.vue b/resources/vue/components/courseware/CoursewareTabsContainer.vue
deleted file mode 100644
index 5cb4ee7..0000000
--- a/resources/vue/components/courseware/CoursewareTabsContainer.vue
+++ /dev/null
@@ -1,403 +0,0 @@
-<template>
-    <courseware-default-container
-        :container="container"
-        containerClass="cw-container-tabs"
-        :canEdit="canEdit"
-        :isTeacher="isTeacher"
-        @showEdit="setShowEdit"
-        @storeContainer="storeContainer"
-        @closeEdit="initCurrentData"
-    >
-        <template v-slot:containerContent>
-            <template v-if="showEditMode && canEdit && !currentElementisLink">
-                <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
-                <span id="operation" class="assistive-text">
-                    {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
-                </span>
-            </template>
-            <courseware-tabs>
-                <courseware-tab
-                    v-for="(section, index) in currentSections"
-                    :key="index"
-                    :index="index"
-                    :name="section.name"
-                    :icon="section.icon"
-                    :selected="sortInTab === index"
-                >
-                    <ul v-if="!showEditMode || currentElementisLink" class="cw-container-tabs-block-list">
-                        <li v-for="block in section.blocks" :key="block.id" class="cw-block-item">
-                            <component
-                                :is="component(block)"
-                                :block="block"
-                                :canEdit="canEdit"
-                                :isTeacher="isTeacher"
-                            />
-                        </li>
-                    </ul>
-                    <template v-else>
-                        <template v-if="canEdit">
-                            <draggable
-                                class="cw-container-list-block-list cw-container-list-sort-mode"
-                                :class="[section.blocks.length === 0 ? 'cw-container-list-sort-mode-empty' : '']"
-                                tag="ol"
-                                role="listbox"
-                                v-model="section.blocks"
-                                v-bind="dragOptions"
-                                handle=".cw-sortable-handle"
-                                group="blocks"
-                                @start="isDragging = true"
-                                @end="dropBlock"
-                                :containerId="container.id"
-                                :sectionId="index"
-                            >
-                                <li v-for="block in section.blocks" :key="block.id" class="cw-block-item cw-block-item-sortable">
-                                    <span
-                                        :class="{ 'cw-sortable-handle-dragging': isDragging }"
-                                        class="cw-sortable-handle"
-                                        tabindex="0"
-                                        role="button"
-                                        aria-describedby="operation"
-                                        :ref="'sortableHandle' + block.id"
-                                        @keydown="keyHandler($event, block.id, index)"
-                                    ></span>
-                                    <component
-                                        :is="component(block)"
-                                        :block="block"
-                                        :canEdit="canEdit"
-                                        :isTeacher="isTeacher"
-                                        :class="{ 'cw-block-item-selected': keyboardSelected === block.id}"
-                                        :blockId="block.id"
-                                    />
-                                </li>
-                            </draggable>
-                            <template v-if="canAddElements">
-                                <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/>
-                            </template>
-                        </template>
-                    </template>
-                </courseware-tab>
-            </courseware-tabs>
-        </template>
-        <template v-slot:containerEditDialog>
-            <form class="default cw-container-dialog-edit-form" @submit.prevent="">
-                <fieldset v-for="(section, index) in currentContainer.attributes.payload.sections.filter(section => !section.locked)" :key="index">
-                    <label>
-                        <translate>Title</translate>
-                        <input type="text" v-model="section.name" />
-                    </label>
-                    <label>
-                        <translate>Icon</translate>
-                        <studip-select :options="icons" v-model="section.icon">
-                            <template #open-indicator="selectAttributes">
-                                <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
-                            </template>
-                            <template #no-options>
-                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
-                            </template>
-                            <template #selected-option="option">
-                                <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
-                            </template>
-                            <template #option="option">
-                                <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
-                            </template>
-                        </studip-select>
-                    </label>
-                    <label
-                        class="cw-container-section-delete"
-                        v-if="currentContainer.attributes.payload.sections.length > 1"
-                    >
-                    <button class="button trash" @click="deleteSection(index)"><translate>Tab löschen</translate></button>
-                    </label>
-                </fieldset>
-            </form>
-            <button class="button add" @click="addSection"><translate>Tab hinzufügen</translate></button>
-        </template>
-    </courseware-default-container>
-</template>
-
-<script>
-import ContainerComponents from './container-components.js';
-import containerMixin from '../../mixins/courseware/container.js';
-import contentIcons from './content-icons.js';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
-import StudipIcon from './../StudipIcon.vue';
-
-import { mapGetters, mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-tabs-container',
-    mixins: [containerMixin],
-    components: Object.assign(ContainerComponents, {
-        CoursewareTabs,
-        CoursewareTab,
-        CoursewareCollapsibleBox,
-        StudipIcon,
-    }),
-    props: {
-        container: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-        canAddElements: Boolean,
-    },
-    data() {
-        return {
-            showEdit: false,
-            currentContainer: null,
-            currentSections: [],
-            unallocatedBlocks: [],
-            textDeleteSection: this.$gettext('Sektion entfernen'),
-            selectAttributes: {'ref': 'openIndicator', 'role': 'presentation', 'class': 'vs__open-indicator'},
-            sortMode: false,
-            isDragging: false,
-            dragOptions: {
-                animation: 0,
-                group: this.container.id,
-                disabled: false,
-                ghostClass: "block-ghost"
-            },
-            processing: false,
-            keyboardSelected: null,
-            sortInTab: 0,
-            assistiveLive: ''
-        };
-    },
-    computed: {
-        ...mapGetters({
-            blockById: 'courseware-blocks/byId',
-            viewMode: 'viewMode',
-            currentElementisLink: 'currentElementisLink'
-        }),
-        showEditMode() {
-            return this.viewMode === 'edit';
-        },
-        blocks() {
-            if (!this.container) {
-                return [];
-            }
-
-            return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })).filter((a) => a);
-        },
-        icons() {
-            return contentIcons;
-        },
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateContainer: 'updateContainer',
-            loadContainer: 'courseware-containers/loadById',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-        }),
-        initCurrentData() {
-            this.currentContainer = _.cloneDeep(this.container);
-
-            let view = this;
-            let sections = this.currentContainer.attributes.payload.sections;
-
-            const unallocated = new Set(this.blocks.map(({ id }) => id));
-
-            for (let section of sections) {
-                section.locked = false;
-                section.blocks = section.blocks.map((id) =>  view.blockById({id})).filter(Boolean);
-                for (let sectionBlock of section.blocks) {
-                    if (sectionBlock?.id && unallocated.has(sectionBlock.id)) {
-                        unallocated.delete(sectionBlock.id);
-                    }
-                }
-            }
-
-            if (unallocated.size > 0) {
-                this.unallocatedBlocks = [...unallocated].map((id) => view.blockById({ id }));
-                sections.push({
-                    blocks: this.unallocatedBlocks,
-                    name: this.$gettext('nicht zugewiesene Inhalte'),
-                    icon: 'decline',
-                    locked: true
-                });
-            }
-
-            this.currentSections = sections;
-        },
-        setShowEdit(state) {
-            this.showEdit = state;
-        },
-        addSection() {
-            this.currentContainer.attributes.payload.sections.push({ name: '', icon: '', blocks: [] });
-        },
-        deleteSection(index) {
-            if (this.currentContainer.attributes.payload.sections.length === 1) {
-                return;
-            }
-            if (this.currentContainer.attributes.payload.sections[index].blocks.length > 0) {
-                if (index === 0) {
-                    this.currentContainer.attributes.payload.sections[
-                        index + 1
-                    ].blocks = this.currentContainer.attributes.payload.sections[index + 1].blocks.concat(
-                        this.currentContainer.attributes.payload.sections[index].blocks
-                    );
-                } else {
-                    this.currentContainer.attributes.payload.sections[
-                        index - 1
-                    ].blocks = this.currentContainer.attributes.payload.sections[index - 1].blocks.concat(
-                        this.currentContainer.attributes.payload.sections[index].blocks
-                    );
-                }
-            }
-            this.currentContainer.attributes.payload.sections.splice(index, 1);
-        },
-        async storeContainer() {
-            const timeout = setTimeout(() => this.processing = true, 800);
-            this.currentContainer.attributes.payload.sections = this.currentContainer.attributes.payload.sections.filter(section => !section.locked);
-            this.currentContainer.attributes.payload.sections.forEach(section => {
-                section.blocks = section.blocks.map((block) => {return block.id;});
-                delete section.locked;
-            });
-            await this.updateContainer({
-                container: this.currentContainer,
-                structuralElementId: this.currentContainer.relationships['structural-element'].data.id,
-            });
-            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
-            await this.loadContainer({id : this.container.id });
-            this.initCurrentData();
-            clearTimeout(timeout);
-            this.processing = false;
-        },
-        async storeSort() {
-            if (this.blockedByAnotherUser) {
-                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
-                this.loadContainer({id : this.container.id });
-                return false;
-            }
-            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-            this.storeContainer();
-        },
-        component(block) {
-            if (block.attributes) {
-                return 'courseware-' + block.attributes["block-type"] + '-block';
-            }
-            return null;
-        },
-        updateContent(blockAdder) {
-            if(blockAdder.container !== undefined && blockAdder.container.id === this.container.id) {
-                this.initCurrentData();
-            }
-        },
-        keyHandler(e, blockId, sectionIndex) {
-            switch (e.keyCode) {
-                case 27: // esc
-                    this.abortKeyboardSorting(blockId, sectionIndex);
-                    break;
-                case 13: // enter
-                    e.preventDefault();
-                    if (this.keyboardSelected) {
-                        this.storeKeyboardSorting(blockId, sectionIndex);
-                    } else {
-                        this.keyboardSelected = blockId;
-                        const block = this.blockById({id: blockId});
-                        const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
-                        this.assistiveLive =
-                            this.$gettextInterpolate(
-                                this.$gettext('%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
-                                , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                            );
-                    }
-                    break;
-            }
-            if (this.keyboardSelected) {
-                switch (e.keyCode) {
-                    case 9: //tab
-                        this.abortKeyboardSorting(blockId, sectionIndex);
-                        break;
-                    case 38: // up
-                        e.preventDefault();
-                        this.moveItemUp(blockId, sectionIndex);
-                        break;
-                    case 40: // down
-                        e.preventDefault();
-                        this.moveItemDown(blockId, sectionIndex);
-                        break;
-                }
-            }
-        },
-        moveItemUp(blockId, sectionIndex) {
-            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
-            const block = this.blockById({id: blockId});
-            if (currentIndex !== 0) {
-                const newPos = currentIndex - 1;
-                this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-                this.assistiveLive =
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                    );
-            } else if (sectionIndex !== 0) {
-                const newSectionIndex = sectionIndex - 1;
-                this.sortInTab = newSectionIndex;
-                this.currentSections[newSectionIndex].blocks.push(this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-            }
-        },
-        moveItemDown(blockId, sectionIndex) {
-            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
-            const block = this.blockById({id: blockId});
-            if (this.currentSections[sectionIndex].blocks.length - 1 > currentIndex) {
-                const newPos = currentIndex + 1;
-                this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-                this.assistiveLive =
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                    );
-            } else if (this.currentSections.length - 1 > sectionIndex) {
-                const newSectionIndex = sectionIndex + 1;
-                this.sortInTab = newSectionIndex;
-                this.currentSections[newSectionIndex].blocks.splice(0, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-            }
-        },
-        abortKeyboardSorting(blockId, sectionIndex) {
-            const block = this.blockById({id: blockId});
-            this.keyboardSelected = null;
-            this.assistiveLive =
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, Neuordnung abgebrochen.')
-                    , {blockTitle: block.attributes.title}
-                );
-            this.initCurrentData();
-        },
-        storeKeyboardSorting(blockId, sectionIndex) {
-            const block = this.blockById({id: blockId});
-            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
-            this.keyboardSelected = null;
-            this.assistiveLive =
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.')
-                    , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                );
-            this.storeSort();
-        }
-    },
-    watch: {
-        blocks(newBlocks, oldBlocks) {
-            if (!this.showEdit && !this.checkSimpleArrayEquality(newBlocks, oldBlocks)) {
-                this.$nextTick(() => {
-                    setTimeout(() =>  this.initCurrentData(), 250);
-                });
-            }
-        },
-        currentSections: {
-            handler() {
-                if (this.keyboardSelected) {
-                    this.$nextTick(() => {
-                        this.$refs['sortableHandle' + this.keyboardSelected][0].focus();
-                    });
-                }
-            },
-            deep: true
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTalkBubble.vue b/resources/vue/components/courseware/CoursewareTalkBubble.vue
deleted file mode 100644
index 72a240a..0000000
--- a/resources/vue/components/courseware/CoursewareTalkBubble.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<template>
-    <div :class="{ 'cw-talk-bubble-own-post': payload.own }" class="cw-talk-bubble">
-        <div class="cw-talk-bubble-user" v-if="!payload.own">
-            <div class="cw-talk-bubble-avatar">
-                <img :src="payload.user_avatar" />
-            </div>
-            <span>{{ payload.user_name }}</span>
-        </div>
-        <div class="cw-talk-bubble-talktext">
-            <p>{{ payload.content }}</p>
-            <p class="cw-talk-bubble-talktext-time"><iso-date :date="payload.chdate" /></p>
-        </div>
-    </div>
-</template>
-
-<script>
-import IsoDate from './IsoDate.vue';
-
-export default {
-    name: 'courseware-talk-bubble',
-    components: { IsoDate },
-    props: {
-        payload: Object,
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTasksActionWidget.vue b/resources/vue/components/courseware/CoursewareTasksActionWidget.vue
deleted file mode 100644
index e428cbe..0000000
--- a/resources/vue/components/courseware/CoursewareTasksActionWidget.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-    <sidebar-widget id="courseware-action-widget" :title="$gettext('Aktionen')">
-        <template #content>
-            <ul class="widget-list widget-links cw-action-widget">
-                <li class="cw-action-widget-add">
-                    <button @click="setShowTasksDistributeDialog(true)">
-                        {{ $gettext('Aufgabe verteilen') }}
-                    </button>
-                </li>
-            </ul>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-tasks-action-widget',
-    components: {
-        SidebarWidget,
-    },
-    methods: {
-        ...mapActions({
-            setShowTasksDistributeDialog: 'setShowTasksDistributeDialog',
-        }),
-    }
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue b/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue
index 1e82d94..79c8cac 100644
--- a/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue
+++ b/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue
@@ -237,9 +237,9 @@
 </template>
 
 <script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareStructuralElementSelector from './CoursewareStructuralElementSelector.vue';
-import StudipWizardDialog from './../StudipWizardDialog.vue';
+import CoursewareCompanionBox from './layouts/CoursewareCompanionBox.vue';
+import CoursewareStructuralElementSelector from './structural-element/CoursewareStructuralElementSelector.vue';
+import StudipWizardDialog from '../StudipWizardDialog.vue';
 
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/CoursewareTextBlock.vue b/resources/vue/components/courseware/CoursewareTextBlock.vue
deleted file mode 100644
index df376c5..0000000
--- a/resources/vue/components/courseware/CoursewareTextBlock.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<template>
-    <div class="cw-block cw-block-text">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="false"
-            ref="defaultBlock"
-            @showEdit="initCurrent"
-            @storeEdit="storeText"
-            @closeEdit="initCurrent"
-        >
-            <template #content>
-                <section class="formatted-content ck-content" v-html="currentText" ref="content"></section>
-            </template>
-            <template v-if="canEdit" #edit>
-                <ckeditor :editor="editor" v-model="currentText" :config="editorConfig" @ready="onReady"></ckeditor>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Text-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { blockMixin } from './block-mixin.js';
-import { ClassicEditor, BalloonEditor } from '@/assets/javascripts/chunks/wysiwyg'
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-text-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentText: '',
-            editor: ClassicEditor,
-            editorConfig: {
-                // The configuration of the editor.
-            }
-        };
-    },
-    computed: {
-        text() {    
-            return this.block?.attributes?.payload?.text;
-        },
-        ckeToolbarTop() {
-            const topBar = document.getElementById('top-bar');
-            const responsiveContentbar = document.getElementById('responsive-contentbar');
-            let top = topBar.clientHeight + topBar.clientTop;
-            if (responsiveContentbar) {
-                top += responsiveContentbar?.clientHeight + responsiveContentbar?.clientTop;
-            } else {
-                top += 85;
-            }
-            
-            return top;
-        },
-    },
-    mounted() {
-        this.initCurrent();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrent() {
-            this.currentText = this.text;
-            this.loadMathjax();
-        },
-        onReady( editor )  {
-            editor.ui.viewportOffset = { top: this.ckeToolbarTop };
-            editor.ui.update();
-        },
-        async storeText() {
-            let attributes = this.block.attributes;
-            attributes.payload.text = this.currentText;
-            await this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-            this.$refs.defaultBlock.displayFeature(false);
-            this.loadMathjax();
-        },
-        loadMathjax() {
-            let mathjaxP;
-            let view = this;
-
-            if (window.MathJax && window.MathJax.Hub) {
-                mathjaxP = Promise.resolve(window.MathJax);
-            } else if (window.STUDIP && window.STUDIP.loadChunk) {
-                mathjaxP = window.STUDIP.loadChunk('mathjax');
-            }
-
-            mathjaxP && mathjaxP
-            .then(({ Hub }) => {
-                Hub.Queue(['Typeset', Hub, view.$refs.content]);
-            })
-            .catch(() => {
-                console.log('Warning: Could not load MathJax.');
-            });
-        },
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTile.vue b/resources/vue/components/courseware/CoursewareTile.vue
deleted file mode 100644
index f396769..0000000
--- a/resources/vue/components/courseware/CoursewareTile.vue
+++ /dev/null
@@ -1,145 +0,0 @@
-<template>
-    <component :is="tag" class="cw-tile" :class="[color]">
-        <div
-            class="preview-image"
-            :class="[hasImage ? '' : 'default-image']"
-            :style="previewImageStyle"
-        >
-            <div class="overlay-text" v-if="hasImageOverlay">
-                <slot name="image-overlay"></slot>
-            </div>
-            <div class="overlay-action-menu" v-if="hasImageOverlayWithActionMenu">
-                <slot name="image-overlay-with-action-menu"></slot>
-            </div>
-        </div>
-        <component
-            :is="hasDescriptionLink ? 'a' : 'div'"
-            :href="hasDescriptionLink ? descriptionLink : ''"
-            :title="descriptionTitle"
-            class="description"
-        >
-            <header
-                :class="[icon ? 'description-icon-' + icon : '']"
-            >
-                {{ title }}
-            </header>
-            <div
-                v-if="displayProgress"
-                :title="progressTitle"
-                class="progress-wrapper" >
-                <progress :value="progress" max="100">{{ progress }}</progress>
-            </div>
-            <div class="description-text-wrapper">
-                <p><slot name="description"></slot></p>
-            </div>
-            <footer>
-                <slot name="footer"></slot>
-            </footer>
-        </component>
-    </component>
-</template>
-
-<script>
-import { mapGetters } from 'vuex';
-
-export default {
-    name: "courseware-tile",
-    props: {
-        tag: {
-            type: String,
-            default: "div",
-            validator: tag => {
-                return ["div", "li"].includes(tag);
-            }
-        },
-        color: {
-            type: String,
-            default: "studip-blue",
-            validator: value => {
-                return [
-                    "black",
-                    "charcoal",
-                    "royal-purple",
-                    "iguana-green",
-                    "queen-blue",
-                    "verdigris",
-                    "mulberry",
-                    "pumpkin",
-                    "sunglow",
-                    "apple-green",
-                    "studip-blue",
-                    "studip-lightblue",
-                    "studip-green",
-                    "studip-yellow",
-                    "studip-gray",
-                ].includes(value);
-            }
-        },
-        title: {
-            type: String,
-            default: "–"
-        },
-        icon: {
-            type: String
-        },
-        imageUrl: {
-            type: String
-        },
-        displayProgress: {
-            type: Boolean,
-            default: false
-        },
-        progress: {
-            type: Number,
-            validator: value => {
-                return value >= 0 && value <= 100;
-            }
-        },
-        descriptionLink: {
-            type: String,
-            default: ""
-        },
-        descriptionTitle: {
-            type: String,
-            default: ''
-        }
-    },
-    computed: {
-        ...mapGetters({
-            userIsTeacher: 'userIsTeacher'
-        }),
-        hasImage() {
-            return this.imageUrl !== "" && this.imageUrl !== undefined;
-        },
-        hasImageOverlay() {
-            return this.$slots["image-overlay"] !== undefined;
-        },
-        hasImageOverlayWithActionMenu() {
-            return this.$slots["image-overlay-with-action-menu"] !== undefined;
-        },
-        previewImageStyle() {
-            if (this.hasImage) {
-                return { "background-image": "url(" + this.imageUrl + ")" };
-            }
-            else {
-                return {};
-            }
-        },
-        progressTitle() {
-            if (this.userIsTeacher) {
-                return this.$gettextInterpolate(this.$gettext("Fortschritt aller Teilnehmenden: %{progress}%"), { progress: this.progress });    
-            }
-            return this.$gettextInterpolate(this.$gettext("Mein Fortschritt: %{progress}%"), { progress: this.progress });
-        },
-        hasDescriptionLink() {
-            return this.descriptionLink !== '';
-        }
-    },
-    methods: {
-        showProgress(e) {
-            e.preventDefault();
-            this.$emit("showProgress");
-        }
-    },
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTimelineBlock.vue b/resources/vue/components/courseware/CoursewareTimelineBlock.vue
deleted file mode 100644
index 1cc5d4d..0000000
--- a/resources/vue/components/courseware/CoursewareTimelineBlock.vue
+++ /dev/null
@@ -1,312 +0,0 @@
-<template>
-    <div class="cw-block cw-block-timeline">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @closeEdit="initCurrentData"
-            @showEdit="setShowEdit"
-            @storeEdit="storeBlock"
-        >
-            <template #content>
-                <ol class="cw-timeline">
-                    <li
-                        v-for="(item, index) in sortedItems"
-                        :key="index"
-                        class="cw-timeline-item"
-                    >
-                        <div class="cw-timeline-item-icon" :class="'cw-timeline-item-icon-color-' + item.color">
-                            <studip-icon :shape="item.icon" role="info" size="32" :class="item.color"/>
-                        </div>
-                        <div
-                            class="cw-timeline-item-content"
-                            :class="[index % 2 === 0 ? 'left' : 'right', 'cw-timeline-item-content-color-' + item.color]"
-                        >
-                            <h3 v-if="currentDateFormat !== 'none'">{{ getItemDate(item) }}</h3>
-                            <h3 v-else>{{ item.title }}</h3>
-                            <article>
-                                <header v-if="currentDateFormat !== 'none'">{{ item.title }}</header>
-                                <p>{{ item.description }}</p>
-                            </article>
-                        </div>
-                    </li>
-                </ol>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label class="col-2">
-                        {{ $gettext('Zeitliche Sortierung') }}
-                        <select v-model="currentSort">
-                            <option value="none">{{ $gettext('Keine') }}</option>
-                            <option value="asc">{{ $gettext('Aufsteigend') }}</option>
-                            <option value="desc">{{ $gettext('Absteigend') }}</option>
-                        </select>
-                    </label>
-                    <label class="col-2">
-                        {{ $gettext('Zeitangabe') }}
-                        <select v-model="currentDateFormat">
-                            <option value="year">{{ $gettext('Jahr') }}</option>
-                            <option value="date">{{ $gettext('Datum') }}</option>
-                            <option value="time">{{ $gettext('Zeit') }}</option>
-                            <option value="datetime">{{ $gettext('Datum und Zeit') }}</option>
-                            <option value="none">{{ $gettext('Keine') }}</option>
-                        </select>
-                    </label>
-                </form>
-                <button class="button add" @click="addItem">{{ $gettext('Ereignis hinzufügen') }}</button>
-                <courseware-tabs
-                    v-if="currentItems.length > 0"
-                    :setSelected="setItemTab"
-                    @selectTab="setItemTab = (parseInt($event.name.replace($gettext('Ereignis') +  ' ', '')) - 1)"
-                >
-                    <courseware-tab
-                        v-for="(item, index) in currentItems"
-                        :key="index"
-                        :index="index"
-                        :name="$gettext('Ereignis') +  ' ' + (index + 1).toString()"
-                        :selected="index === 0"
-                        canBeEmpty
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label class="col-4">
-                                {{ $gettext('Titel') }}
-                                <input type="text" v-model="item.title" />
-                            </label>
-                            <label class="col-4">
-                            
-                                {{ $gettext('Beschreibung') }}
-                                <textarea v-model="item.description" />
-                            </label>
-                            <br>
-                            <label class="col-2">
-                                {{ $gettext('Farbe') }}
-                                <studip-select
-                                    :options="colors"
-                                    label="icon"
-                                    :clearable="false"
-                                    :reduce="option => option.class"
-                                    v-model="item.color"
-                                >
-                                    <template #open-indicator="selectAttributes">
-                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
-                                    </template>
-                                    <template #no-options>
-                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                                    </template>
-                                    <template #selected-option="{name, hex}">
-                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                                    </template>
-                                    <template #option="{name, hex}">
-                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
-                                    </template>
-                                </studip-select>
-                            </label>
-                            <label class="col-2">
-                                {{ $gettext('Icon') }}
-                                <studip-select :options="icons" :clearable="false" v-model="item.icon">
-                                    <template #open-indicator="selectAttributes">
-                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
-                                    </template>
-                                    <template #no-options>
-                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
-                                    </template>
-                                    <template #selected-option="option">
-                                        <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
-                                    </template>
-                                    <template #option="option">
-                                        <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
-                                    </template>
-                                </studip-select>
-                            </label>
-                            <br>
-                            <label class="col-1">
-                                {{ $gettext('Datum') }}
-                                <input type="date" v-model="item.date" required/>
-                            </label>
-                            <label class="col-1">
-                                {{ $gettext('Zeit') }}
-                                <input type="time" v-model="item.time" />
-                            </label>
-                            <label v-if="currentItems.length > 1">
-                                <button class="button trash" @click="removeItem(index)">
-                                    {{ $gettext('Ereignis entfernen') }}
-                                </button>
-                            </label>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Zeitstrahl-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import { mapActions } from 'vuex';
-import { blockMixin } from './block-mixin.js';
-import colorMixin from '@/vue/mixins/courseware/colors.js';
-import contentIcons from './content-icons.js';
-import StudipIcon from '../StudipIcon.vue';
-
-export default {
-    name: 'courseware-timeline-block',
-    mixins: [blockMixin, colorMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareTabs,
-        CoursewareTab,
-        StudipIcon,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            showEdit: false,
-            setItemTab: 0,
-            currentItems: [],
-            currentSort: '',
-            currentDateFormat: '',
-        }
-    },
-    computed: {
-        items() {
-            return this.block?.attributes?.payload?.items;
-        },
-        sort() {
-            return this.block?.attributes?.payload?.sort;
-        },
-        dateformat() {
-            return this.block?.attributes?.payload?.dateformat;
-        },
-        icons() {
-            return contentIcons;
-        },
-        colors() {
-            return this.mixinColors.filter(color => color.class !== 'white' && color.class !== 'studip-lightblue');
-        },
-        sortedItems() {
-            if (this.currentSort === 'none') {
-                return this.currentItems;
-            }
-            let view = this;
-            let items = _.cloneDeep(this.currentItems);
-            return items.sort((a, b) => {
-                let dateA = null;
-                let dateB = null;
-
-                if (a.time) {
-                    dateA = new Date(a.date + 'T' + a.time);
-                } else {
-                    dateA = new Date(a.date);
-                }
-                if (b.time) {
-                    dateB = new Date(b.date + 'T' + b.time);
-                } else {
-                    dateB = new Date(b.date);
-                }
-                if (view.currentSort === 'asc') {
-                    return dateA > dateB ? 1 : dateA < dateB ? -1 : 0;
-                }
-                if (view.currentSort === 'desc') {
-                    return dateA < dateB ? 1 : dateA > dateB ? -1 : 0;
-                }
-            });
-        }
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        setShowEdit(state) {
-            this.showEdit = state;
-        },
-        initCurrentData() {
-            this.currentItems = this.items;
-            this.currentSort = this.sort;
-            this.currentDateFormat = this.dateformat;
-            this.setItemTab = 0;
-        },
-        getItemDate(item) {
-            switch (this.currentDateFormat) {
-                case 'year':
-                    if (item.date) {
-                        return (new Date(item.date)).getFullYear();
-                    }
-                    break;
-                case 'date':
-                    if (item.date) {
-                        return this.getReadableDate(item.date);
-                    }
-                    break;
-                case 'time':
-                     if (item.time) {
-                        return item.time;
-                     }
-                     break;
-                case 'datetime':
-                    if (item.date && item.time) {
-                        return this.getReadableDate(item.date) + ' ' + item.time;
-                    }
-                    if (!item.date && item.time) {
-                        return '--.--.---- ' + item.time;
-                    }
-                    if (item.date && !item.time) {
-                        return this.getReadableDate(item.date) + ' --:--';
-                    }
-                    return '--.--.---- --:--'
-            }
-
-            return '';
-        },
-        addItem() {
-            this.currentItems.push({
-                title: '',
-                description: '',
-                date: '',
-                time: '',
-                color: 'studip-blue',
-                icon: 'courseware'
-            });
-            this.$nextTick(() => { this.setItemTab = this.currentItems.length - 1; });
-        },
-        removeItem(itemIndex){
-            this.currentItems = this.currentItems.filter((val, index) => {
-                return !(index === itemIndex);
-            });
-            this.$nextTick(() => { this.setItemTab = 0; });
-        },
-        storeBlock() {
-            let attributes = {
-                payload: {
-                    dateformat: this.currentDateFormat,
-                    sort: this.currentSort,
-                    items: this.currentItems
-                }
-            };
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-    },
-    watch: {
-        items() {
-            if (!this.showEdit) {
-                this.initCurrentData();
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue
deleted file mode 100644
index 36348a3..0000000
--- a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue
+++ /dev/null
@@ -1,437 +0,0 @@
-<template>
-    <div class="cw-tools-element-adder">
-        <courseware-tabs class="cw-tools-element-adder-tabs">
-            <courseware-tab :name="$gettext('Blöcke')" :selected="showBlockadder" :index="0" :style="{ maxHeight: maxHeight + 'px' }">
-                <form @submit.prevent="loadSearch">
-                    <div class="input-group files-search search cw-block-search">
-                        <input
-                            ref="searchBox"
-                            type="text"
-                            v-model="searchInput"
-                            @click.stop
-                            :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')"
-                        />
-                        <span class="input-group-append" @click.stop>
-                            <button v-if="searchInput"
-                                type="button"
-                                class="button reset-search"
-                                id="reset-search"
-                                :title="$gettext('Suche zurücksetzen')"
-                                @click="resetSearch"
-                            >
-                                <studip-icon shape="decline" :size="20"></studip-icon>
-                            </button>
-                            <button 
-                                type="submit"
-                                class="button"
-                                id="search-btn"
-                                :title="$gettext('Suche starten')"
-                                @click="loadSearch"
-                            >
-                                <studip-icon shape="search" :size="20"></studip-icon>
-                            </button>
-                        </span>
-                    </div>
-                </form>
-
-                <div class="filterpanel">
-                    <span class="sr-only">{{ $gettext('Kategorien Filter') }}</span>
-                    <button
-                        v-for="category in blockCategories"
-                        :key="category.type"
-                        class="button"
-                        :class="{'button-active': category.type ===  currentFilterCategory }"
-                        :aria-pressed="category.type ===  currentFilterCategory ? 'true' : 'false'"
-                        @click="selectCategory(category.type)"
-                    >
-                        {{ category.title }}
-                    </button>
-                </div>
-
-                <div v-if="filteredBlockTypes.length > 0" class="cw-blockadder-item-list">
-                    <courseware-blockadder-item
-                        v-for="(block, index) in filteredBlockTypes"
-                        :key="index"
-                        :title="block.title"
-                        :type="block.type"
-                        :description="block.description"
-                        @blockAdded="$emit('blockAdded')"
-                    />
-                </div>
-                <courseware-companion-box
-                    v-else
-                    :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')"
-                    mood="pointing"
-                />
-            </courseware-tab>
-            <courseware-tab :name="$gettext('Abschnitte')" :selected="showContaineradder" :index="1" :style="{ maxHeight: maxHeight + 'px' }">
-                <div class="cw-container-style-selector" role="group" aria-labelledby="cw-containeradder-style">
-                    <p class="sr-only" id="cw-containeradder-style">{{ $gettext('Abschnitt-Stil') }}</p>
-                    <template
-                        v-for="style in containerStyles"
-                    >
-                        <input
-                            :key="style.key  + '-input'"
-                            type="radio"
-                            name="container-style"
-                            :id="'style-' + style.colspan"
-                            v-model="selectedContainerStyle"
-                            :value="style.colspan"
-                        />
-                        <label
-                            :key="style.key + '-label'"
-                            :for="'style-' + style.colspan"
-                            :class="[selectedContainerStyle === style.colspan ? 'cw-container-style-selector-active' : '', style.colspan]"
-                        >
-                            {{ style.title }}
-                        </label>
-                        
-                    </template>
-                </div>
-                <courseware-container-adder-item
-                    v-for="container in containerTypes"
-                    :key="container.type"
-                    :title="container.title"
-                    :type="container.type"
-                    :colspan="selectedContainerStyle"
-                    :description="container.description"
-                    :firstSection="$gettext('erstes Element')"
-                    :secondSection="$gettext('zweites Element')"
-                ></courseware-container-adder-item>
-            </courseware-tab>
-            <courseware-tab :name="$gettext('Merkliste')" :selected="showClipboard" :index="2" :style="{ maxHeight: maxHeight + 'px' }">
-                <courseware-collapsible-box :title="$gettext('Blöcke')" :open="clipboardBlocks.length > 0">
-                    <template v-if="clipboardBlocks.length > 0">
-                        <div class="cw-element-inserter-wrapper">
-                            <courseware-clipboard-item
-                                v-for="(clipboard, index) in clipboardBlocks"
-                                :key="index"
-                                :clipboard="clipboard"
-                                @inserted="$emit('blockAdded')"
-                            />
-                        </div>
-                        <button class="button trash" @click="clearClipboard('courseware-blocks')">
-                            {{ $gettext('Alle Blöcke aus Merkliste entfernen') }}
-                        </button>
-                    </template>
-                    <courseware-companion-box
-                        v-else
-                        mood="pointing"
-                        :msgCompanion="$gettext('Die Merkliste enthält keine Blöcke.')"
-                    />
-                </courseware-collapsible-box>
-                <courseware-collapsible-box :title="$gettext('Abschnitte')" :open="clipboardContainers.length > 0">
-                    <template v-if="clipboardContainers.length > 0">
-                        <div class="cw-element-inserter-wrapper">
-                            <courseware-clipboard-item
-                                v-for="(clipboard, index) in clipboardContainers"
-                                :key="index"
-                                :clipboard="clipboard"
-                            />
-                        </div>
-                        <button class="button trash" @click="clearClipboard('courseware-containers')">
-                            {{ $gettext('Alle Abschnitte aus Merkliste entfernen') }}
-                        </button>
-                    </template>
-                    <courseware-companion-box
-                        v-else
-                        mood="pointing"
-                        :msgCompanion="$gettext('Die Merkliste enthält keine Abschnitte.')"
-                    />
-                </courseware-collapsible-box>
-            </courseware-tab>
-        </courseware-tabs>
-        <studip-dialog
-            v-if="showDeleteClipboardDialog"
-            :title="textDeleteClipboardTitle"
-            :question="textDeleteClipboardAlert"
-            height="200"
-            width="500"
-            @confirm="executeDeleteClipboard"
-            @close="closeDeleteClipboardDialog"
-        ></studip-dialog>
-    </div>
-</template>
-
-<script>
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue';
-import CoursewareClipboardItem from './CoursewareClipboardItem.vue';
-import CoursewareContainerAdderItem from './CoursewareContainerAdderItem.vue';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-import StudipDialog from '../StudipDialog.vue';
-
-export default {
-    name: 'cw-tools-blockadder',
-    components: {
-        CoursewareTabs,
-        CoursewareTab,
-        CoursewareBlockadderItem,
-        CoursewareClipboardItem,
-        CoursewareContainerAdderItem,
-        CoursewareCompanionBox,
-        CoursewareCollapsibleBox,
-
-        StudipDialog,
-    },
-    props: {
-        stickyRibbon: {
-            type: Boolean,
-            default: false,
-        },
-    },
-    data() {
-        return {
-            showBlockadder: true,
-            showContaineradder: false,
-            showClipboard: false,
-            searchInput: '',
-            currentFilterCategory: '',
-            filteredBlockTypes: [],
-            categorizedBlocks: [],
-            selectedContainerStyle: 'full',
-            showDeleteClipboardDialog: false,
-            deleteClipboardType: null
-        };
-    },
-    computed: {
-        ...mapGetters({
-            adderStorage: 'blockAdder',
-            containerAdder: 'containerAdder',
-            unorderedBlockTypes: 'blockTypes',
-            containerTypes: 'containerTypes',
-            favoriteBlockTypes: 'favoriteBlockTypes',
-            showToolbar: 'showToolbar',
-            usersClipboards: 'courseware-clipboards/all',
-            userId: 'userId'
-        }),
-        blockTypes() {
-            let blockTypes = JSON.parse(JSON.stringify(this.unorderedBlockTypes));
-            blockTypes.sort((a, b) => {
-                return a.title > b.title ? 1 : b.title > a.title ? -1 : 0;
-            });
-            return blockTypes;
-        },
-        containerStyles() {
-            return [
-                { key: 0, title: this.$gettext('Volle Breite'), colspan: 'full'},
-                { key: 1, title: this.$gettext('Halbe Breite'), colspan: 'half' },
-                { key: 2, title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' },
-            ];
-        },
-        blockCategories() {
-            return [
-                { title: this.$gettext('Favoriten'), type: 'favorite' },
-                { title: this.$gettext('Texte'), type: 'text' },
-                { title: this.$gettext('Multimedia'), type: 'multimedia' },
-                { title: this.$gettext('Interaktion'), type: 'interaction' },
-                { title: this.$gettext('Gestaltung'), type: 'layout' },
-                { title: this.$gettext('Externe Inhalte'), type: 'external' },
-                { title: this.$gettext('Biografie'), type: 'biography' },
-            ];
-        },
-        maxHeight() {
-            if (this.stickyRibbon) {
-                return parseInt(window.innerHeight * 0.75) - 120;
-            } else {
-                return parseInt(Math.min(window.innerHeight * 0.75, window.innerHeight - 197)) - 120;
-            }
-        },
-        clipboardBlocks() {
-            return this.usersClipboards
-                .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-blocks')
-                .sort((a, b) => b.attributes.mkdate - a.attributes.mkdate);
-        },
-        clipboardContainers() {
-            return this.usersClipboards
-                .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-containers')
-                .sort((a, b) => b.attributes.mkdate < a.attributes.mkdate);
-        },
-        textDeleteClipboardTitle() {
-            if (this.deleteClipboardType === 'courseware-blocks') {
-                return this.$gettext('Merkliste für Blöcke leeren');
-            }
-            if (this.deleteClipboardType === 'courseware-containers') {
-                return this.$gettext('Merkliste für Abschnitte leeren');
-            }
-            return '';
-        },
-        textDeleteClipboardAlert() {
-            if (this.deleteClipboardType === 'courseware-blocks') {
-                return this.$gettext('Möchten Sie die Merkliste für Blöcke unwiderruflich leeren?');
-            }
-            if (this.deleteClipboardType === 'courseware-containers') {
-                return this.$gettext('Möchten Sie die Merkliste für Abschnitte unwiderruflich leeren?');
-            }
-            return '';
-        }
-    },
-    methods: {
-        ...mapActions({
-            removeFavoriteBlockType: 'removeFavoriteBlockType',
-            addFavoriteBlockType: 'addFavoriteBlockType',
-            coursewareContainerAdder: 'coursewareContainerAdder',
-            companionWarning: 'companionWarning',
-            deleteUserClipboards: 'deleteUserClipboards'
-        }),
-        displayContainerAdder() {
-            this.showContaineradder = true;
-            this.showBlockadder = false;
-            this.showClipboard = false;
-        },
-        displayBlockAdder() {
-            this.showContaineradder = false;
-            this.showClipboard = false;
-            this.showBlockadder = true;
-            this.disableContainerAdder();
-        },
-        displayClipboard() {
-            this.showClipboard = true;
-        },
-        isBlockFav(block) {
-            let isFav = false;
-            this.favoriteBlockTypes.forEach((type) => {
-                if (type.type === block.type) {
-                    isFav = true;
-                }
-            });
-
-            return isFav;
-        },
-        disableContainerAdder() {
-            this.coursewareContainerAdder(false);
-        },
-        loadSearch() {
-            let searchTerms = this.searchInput.trim();
-            if (searchTerms.length < 3 && !this.currentFilterCategory) {
-                this.companionWarning({info: this.$gettext('Leider ist Ihr Suchbegriff zu kurz. Der Suchbegriff muss mindestens 3 Zeichen lang sein.')});
-                return;
-            }
-            this.filteredBlockTypes = this.blockTypes;
-
-            // filter results by given filter first so only these results are searched if an additional search term is given
-            if (this.currentFilterCategory) {
-                this.filterBlockTypesByCategory();
-                this.categorizedBlocks = this.filteredBlockTypes;
-            } else {
-                this.categorizedBlocks = this.blockTypes;
-            }
-
-            searchTerms = searchTerms.toLowerCase().split(' ');
-
-            // sort out block types that don't contain all search words
-            searchTerms.forEach(term => {
-                this.filteredBlockTypes = this.filteredBlockTypes.filter(block => (
-                    block.title.toLowerCase().includes(term)
-                    || block.description.toLowerCase().includes(term)
-                ));
-            });
-
-            // add block types to the search if a search term matches a tag even if they aren't in the given category
-            if (this.searchInput.trim().length > 0) {
-                this.filteredBlockTypes.push(...this.getBlockTypesByTags(searchTerms));
-                // remove possible duplicates
-                this.filteredBlockTypes = [...new Map(this.filteredBlockTypes.map(item => [item['title'], item])).values()];
-            }
-        },
-        filterBlockTypesByCategory() {
-            if (this.currentFilterCategory !== 'favorite') {
-                this.filteredBlockTypes = this.filteredBlockTypes.filter(block => block.categories.includes(this.currentFilterCategory));
-            } else {
-                this.filteredBlockTypes = this.favoriteBlockTypes;
-            }
-            
-        },
-        getBlockTypesByTags(searchTags) {
-            return this.categorizedBlocks.filter(block => {
-                const lowercaseTags = block.tags.map(blockTag => blockTag.toLowerCase());
-                for (const tag of searchTags) {
-                    if (lowercaseTags.filter(blockTag => blockTag.includes(tag.toLowerCase())).length > 0) {
-                        return true;
-                    }
-                }
-                return false;
-            });
-        },
-        selectCategory(type) {
-            if (this.currentFilterCategory !== type) {
-                this.currentFilterCategory = type;
-            } else {
-                this.resetCategory();
-            }
-        },
-        resetCategory() {
-            this.currentFilterCategory = '';
-            if (!this.searchInput) {
-                this.filteredBlockTypes = this.blockTypes;
-            } else {
-                this.loadSearch();
-            }
-        },
-        resetSearch() {
-            this.filteredBlockTypes = this.blockTypes;
-            this.searchInput = '';
-            this.currentFilterCategory = '';
-        },
-        clearClipboard(type) {
-            this.deleteClipboardType = type;
-            this.showDeleteClipboardDialog = true;
-        },
-        executeDeleteClipboard() {
-            if (this.deleteClipboardType) {
-                this.deleteUserClipboards({uid: this.userId, type: this.deleteClipboardType});
-            }
-            this.closeDeleteClipboardDialog();
-        },
-        closeDeleteClipboardDialog() {
-            this.showDeleteClipboardDialog = false;
-            this.deleteClipboardType = null;
-        }
-    },
-    mounted() {
-        if (this.containerAdder === true) {
-            this.displayContainerAdder();
-        }
-        this.filteredBlockTypes = this.blockTypes;
-        setTimeout(() => this.$refs.searchBox.focus(), 800);
-    },
-    watch: {
-        adderStorage(newValue) {
-            if (Object.keys(newValue).length !== 0) {
-                this.displayBlockAdder();
-            }
-        },
-        containerAdder(newValue) {
-            if (newValue === true) {
-                this.displayContainerAdder();
-            }
-        },
-        showToolbar(newValue, oldValue) {
-            if (oldValue === true && newValue === false) {
-                this.disableContainerAdder();
-            }
-        },
-        searchInput(newValue, oldValue) {
-            if (newValue.length >= 3 && newValue !== oldValue) {
-                this.loadSearch();
-            }
-            if (newValue.length < oldValue.length && newValue.length < 3) {
-                if (!this.currentFilterCategory) {
-                    this.filteredBlockTypes = this.blockTypes;
-                } else {
-                    this.loadSearch();
-                }
-            }
-        },
-        currentFilterCategory(newValue) {
-            if (newValue) {
-                this.loadSearch();
-            }
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareToolsContents.vue b/resources/vue/components/courseware/CoursewareToolsContents.vue
deleted file mode 100644
index 406045e..0000000
--- a/resources/vue/components/courseware/CoursewareToolsContents.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-<template>
-    <div class="cw-tools cw-tools-contents">
-        <courseware-tree v-if="structuralElements.length" />
-    </div>
-</template>
-
-<script>
-import CoursewareTree from './CoursewareTree.vue';
-import { mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-tools-contents',
-    components: {
-        CoursewareTree,
-    },
-
-    computed: {
-        ...mapGetters({
-            structuralElements: 'courseware-structural-elements/all',
-        }),
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTree.vue b/resources/vue/components/courseware/CoursewareTree.vue
deleted file mode 100644
index 1832372..0000000
--- a/resources/vue/components/courseware/CoursewareTree.vue
+++ /dev/null
@@ -1,248 +0,0 @@
-<template>
-    <div class="cw-tree" ref="tree">
-        <template v-if="editMode">
-                <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
-                <span id="operation" class="assistive-text">
-                    {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
-                </span>
-        </template>
-        <ol v-if="!processing" class="cw-tree-root-list" role="listbox">
-            <courseware-tree-item
-                class="cw-tree-item"
-                :element="rootElementWithNestedChildren"
-                :currentElement="currentElement"
-                @sort="sort"
-                @moveItemUp="moveItemUp"
-                @moveItemDown="moveItemDown"
-                @moveItemPrevLevel="moveItemPrevLevel"
-                @moveItemNextLevel="moveItemNextLevel"
-                @childrenUpdated="updateNestedChildren"
-            ></courseware-tree-item>
-        </ol>
-        <studip-progress-indicator
-            v-else 
-            :description="$gettext('Vorgang wird bearbeitet...')"
-        />
-    </div>
-</template>
-
-<script>
-import CoursewareTreeItem from './CoursewareTreeItem.vue';
-import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    components: { 
-        CoursewareTreeItem,
-        StudipProgressIndicator
-    },
-    name: 'courseware-tree',
-    data() {
-        return {
-            nestedChildren: [],
-            processing: false,
-            rootElementWithNestedChildren: {},
-        }
-    },
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            courseware: 'courseware',
-            relatedStructuralElement: 'courseware-structural-elements/related',
-            structuralElementById: 'courseware-structural-elements/byId',
-            childrenById: 'courseware-structure/children',
-            viewMode: 'viewMode',   
-            structuralElements: 'courseware-structural-elements/all',
-            assistiveLive: 'assistiveLiveContents'
-        }),
-        currentElement() {
-            const id = this.$route?.params?.id;
-            if (!id) {
-                return null;
-            }
-
-            return this.structuralElementById({ id }) ?? null;
-        },
-
-        rootElement() {
-            if (this.context.type !== 'public') {
-                    const root = this.relatedStructuralElement({
-                    parent: { id: this.courseware.id, type: this.courseware.type },
-                    relationship: 'root',
-                });
-
-                return root;
-            } else {
-                return this.structuralElementById({ id: this.context.rootId });
-            }
-
-        },
-        editMode() {
-            return this.viewMode === 'edit';
-        },
-    },
-    methods: {
-         ...mapActions({
-            updateStructuralElement: 'updateStructuralElement',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-            sortChildrenInStructualElements: 'sortChildrenInStructualElements',
-            loadStructuralElement: 'loadStructuralElement',
-            setAssistiveLiveContents: 'setAssistiveLiveContents',
-            companionError: 'companionError',
-         }),
-        updateNestedChildren() {
-            this.nestedChildren = this.getNestedChildren(this.rootElement);
-            this.setRootElementWithNestedChildren();
-        },
-        setRootElementWithNestedChildren() {
-            let element = { id: this.rootElement.id, attributes: this.rootElement.attributes };
-            element.nestedChildren = this.nestedChildren;
-
-            this.rootElementWithNestedChildren = element;
-        },
-        getNestedChildren(structuralElement) {
-            let children = _.cloneDeep(this.structuralElements
-                .filter(
-                    element => element.relationships.parent?.data?.id === structuralElement.id
-                )).sort((a,b) => a.attributes.position - b.attributes.position);
-
-            let nestedChildren = [];
-            for (let child of children) {
-                child.nestedChildren = this.getNestedChildren(child);
-                nestedChildren.push(child);
-            }
-
-            return nestedChildren;
-        },
-        async sort(data) {
-            const tree = this.$refs.tree;
-            const currentScrollPosition = tree.offsetParent.scrollTop;
-            this.processing = true;
-            await this.loadStructuralElement(data.newParent);
-            const newParent = this.structuralElementById({ id: data.newParent });
-            if (!newParent.attributes['can-edit']) {
-                this.processing = false;
-                this.updateNestedChildren();
-                this.companionError({
-                    info: this.$gettext('Verschieben nicht möglich. Sie haben keine Schreibrechte für die Ziel-Seite.')
-                });
-                return;
-            }
-            if (data.oldParent !== data.newParent) {
-                await this.lockObject({ id: data.id, type: 'courseware-structural-elements' });
-                let element = this.structuralElementById({ id: data.id });
-                element.relationships.parent.data.id = newParent.id;
-                await this.updateStructuralElement({
-                    element: element,
-                    id: element.id,
-                });
-                await this.unlockObject({ id: data.id, type: 'courseware-structural-elements' });
-                await this.loadStructuralElement(data.id);
-            }
-            await this.loadStructuralElement(newParent.id);
-            const parent = this.structuralElementById({ id: newParent.id });
-            await this.sortChildrenInStructualElements({parent: parent, children: data.sortArray});
-            this.updateNestedChildren();
-            this.processing = false;
-            this.$nextTick(() => {
-                tree.offsetParent.scrollTop = currentScrollPosition;
-            });
-        },
-        moveItemUp(data) {
-            data.direction = 'up';
-            this.reorderNestedChildren(data);
-        },
-        moveItemDown(data) {
-            data.direction = 'down';
-            this.reorderNestedChildren(data);
-        },
-        moveItemPrevLevel(data) {
-            data.direction = 'prev';
-            this.reorderNestedChildren(data);
-        },
-        moveItemNextLevel(data) {
-            data.direction = 'next';
-            this.reorderNestedChildren(data);
-        },
-        reorderNestedChildren(data) {
-            this.rootElementWithNestedChildren = this.recursiveNestedChildrenUpdate(this.rootElementWithNestedChildren, data);
-        },
-        recursiveNestedChildrenUpdate(element, data) {
-            if (data.direction === 'prev' && data.parents[1] && element.id === data.parents[1]) {
-                //element is grandparent
-                let parentIndex = element.nestedChildren.findIndex((e) => e.id === data.parents[0]);
-                let movingElementIndex = element.nestedChildren[parentIndex].nestedChildren.findIndex((e) => e.id === data.element.id);
-                const newPos = parentIndex + 1;
-                element.nestedChildren.splice(newPos, 0, element.nestedChildren[parentIndex].nestedChildren[movingElementIndex]);
-                element.nestedChildren[parentIndex].nestedChildren.splice(movingElementIndex, 1);
-
-                element.nestedChildren[newPos].newPos = newPos;
-                element.nestedChildren[newPos].newParentId =  parseInt(element.id);
-                element.nestedChildren[newPos].sortArray = element.nestedChildren.map(c => {return {id: c.id, type: 'courseware-structural-elements'}});
-                element.nestedChildren[newPos].moveDirection = data.direction;
-
-                const assistiveLive = this.$gettextInterpolate(
-                    this.$gettext('%{elementTitle} eine Ebene nach oben bewegt. Übergeordnete Seite: %{parentTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}'), 
-                    { elementTitle: data.element.attributes.title, parentTitle: element.attributes.title, pos: newPos + 1, listLength: element.nestedChildren[newPos].sortArray.length }
-                );
-                this.setAssistiveLiveContents(assistiveLive);
-            }
-            if (element.id === data.parents[0]) {
-                if (data.direction === 'up' || data.direction === 'down') {
-                    const elementIndex = element.nestedChildren.findIndex((e) => e.id === data.element.id);
-                    let vertical = data.direction === 'up' ? -1 : data.direction === 'down' ? 1 : 0;
-                    const newPos = elementIndex + vertical;
-                    if (newPos >= 0 && newPos < element.nestedChildren.length) {
-                        element.nestedChildren.splice(newPos, 0, element.nestedChildren.splice(elementIndex, 1)[0]);
-                        element.nestedChildren[newPos].newPos = newPos;
-                        element.nestedChildren[newPos].newParentId = parseInt(element.id);
-                        element.nestedChildren[newPos].sortArray = element.nestedChildren.map(c => {return {id: c.id, type: 'courseware-structural-elements'}});
-                        element.nestedChildren[newPos].moveDirection = data.direction;
-
-                        const assistiveLive = this.$gettextInterpolate(
-                            this.$gettext('%{elementTitle} bewegt. Aktuelle Position in der Liste: %{pos} von %{listLength}'), 
-                            { elementTitle: data.element.attributes.title, pos: newPos + 1, listLength: element.nestedChildren[newPos].sortArray.length }
-                        );
-                        this.setAssistiveLiveContents(assistiveLive);
-                    }
-                }
-                if (data.direction === 'next') {
-                    const elementIndex = element.nestedChildren.findIndex((e) => e.id === data.element.id);
-                    if (elementIndex !== 0) {
-                        const newParentIndex = elementIndex - 1;
-                        element.nestedChildren[newParentIndex].nestedChildren.push(element.nestedChildren[elementIndex]);
-                        element.nestedChildren.splice(elementIndex, 1);
-                        const newPos = element.nestedChildren[newParentIndex].nestedChildren.length - 1;
-                        element.nestedChildren[newParentIndex].nestedChildren[newPos].newPos = newPos;
-                        const newParentId = element.nestedChildren[newParentIndex].id;
-                        element.nestedChildren[newParentIndex].nestedChildren[newPos].newParentId = parseInt(newParentId);
-                        element.nestedChildren[newParentIndex].nestedChildren[newPos].sortArray = element.nestedChildren[newParentIndex].nestedChildren.map(c => {return {id: c.id, type: 'courseware-structural-elements'}});
-
-                        const assistiveLive = this.$gettextInterpolate(
-                            this.$gettext('%{elementTitle} eine Ebene nach unten bewegt. Übergeordnete Seite: %{parentTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}'), 
-                            { elementTitle: data.element.attributes.title, parentTitle: element.nestedChildren[newParentIndex].attributes.title, pos: newPos + 1, listLength: element.nestedChildren[newParentIndex].nestedChildren[newPos].sortArray.length }
-                        );
-                        this.setAssistiveLiveContents(assistiveLive);
-                    }
-                }
-            } else {
-                element.nestedChildren.forEach((child,index) => {
-                    element.nestedChildren[index] = this.recursiveNestedChildrenUpdate(child, data);
-                });
-            }
-
-            return element;
-        },
-    },
-    mounted() {
-        this.updateNestedChildren();
-    },
-    watch: {
-        structuralElements() {
-            this.updateNestedChildren();
-        }
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTreeItem.vue b/resources/vue/components/courseware/CoursewareTreeItem.vue
deleted file mode 100644
index 820b58a..0000000
--- a/resources/vue/components/courseware/CoursewareTreeItem.vue
+++ /dev/null
@@ -1,473 +0,0 @@
-<template>
-    <li v-if="showItem"
-        :draggable="editMode ? true : null"
-        :aria-selected="editMode ? keyboardSelected : null"
-    >
-        <div class="cw-tree-item-wrapper">
-            <span
-                v-if="editMode && depth > 0 && canEdit"
-                class="cw-sortable-handle"
-                :tabindex="0"
-                aria-describedby="operation"
-                ref="sortableHandle"
-                role="button"
-                @keydown="handleKeyEvent"
-            >
-            </span>
-            <router-link
-                :to="'/structural_element/' + element.id"
-                class="cw-tree-item-link"
-                :class="{
-                    'cw-tree-item-link-current': isCurrent,
-                    'cw-tree-item-link-edit': editMode,
-                    'cw-tree-item-link-selected': keyboardSelected
-                }"
-            >
-                {{ element.attributes?.title || "–" }}
-                <span v-if="task">| {{ solverName }}</span>
-                <span
-                    v-if="hasReleaseOrWithdrawDate"
-                    class="cw-tree-item-flag-date"
-                    :title="$gettext('Diese Seite hat eine zeitlich beschränkte Sichtbarkeit')"
-                ></span>
-                <span
-                    v-if="hasWriteApproval"
-                    class="cw-tree-item-flag-write"
-                    :title="$gettext('Diese Seite kann von Teilnehmenden bearbeitet werden')"
-                ></span>
-                <span
-                    v-if="hasNoReadApproval"
-                    class="cw-tree-item-flag-cant-read"
-                    :title="$gettext('Diese Seite kann von Teilnehmenden nicht gesehen werden')"
-                ></span>
-                <template v-if="!userIsTeacher && inCourse">
-                    <span
-                        v-if="complete"
-                        class="cw-tree-item-sequential cw-tree-item-sequential-complete"
-                        :title="$gettext('Diese Seite wurde von Ihnen vollständig bearbeitet')"
-                    >
-                    </span>
-                    <span
-                        v-else
-                        class="cw-tree-item-sequential cw-tree-item-sequential-percentage"
-                        :title="$gettextInterpolate(
-                                    $gettext('Fortschritt: %{progress}%'),
-                                    {progress: itemProgress}
-                                )"
-                    >
-                        {{ itemProgress }} %
-                    </span>
-            </template>
-            </router-link>
-        </div>
-        <ol
-            v-if="hasChildren && !editMode"
-            :class="{
-                'cw-tree-chapter-list': isRoot,
-                'cw-tree-subchapter-list': isFirstLevel,
-            }"
-        >
-            <courseware-tree-item
-                v-for="child in children"
-                :key="child.id"
-                :element="child"
-                :currentElement="currentElement"
-                :depth="depth + 1"
-                class="cw-tree-item"
-            />
-        </ol>
-        <draggable
-            v-if="editMode"
-            :class="{ 'cw-tree-chapter-list-empty': nestedChildren.length === 0 }"
-            tag="ol"
-            :component-data="draggableData"
-            class="cw-tree-draggable-list"
-            handle=".cw-sortable-handle"
-            v-bind="dragOptions"
-            :elementId="element.id"
-            :list="nestedChildren" 
-            :group="{ name: 'g1' }"
-            @end="endDrag"
-        >
-            <courseware-tree-item 
-                v-for="el in nestedChildren"
-                :key="el.id"
-                :element="el"
-                :currentElement="currentElement"
-                :depth="depth + 1"
-                :newPos="el.newPos"
-                :newParentId="el.newParentId"
-                :siblingCount="nestedChildren.length"
-                class="cw-tree-item"
-                :elementid="el.id"
-                @sort="sort"
-                @moveItemUp="moveItemUp"
-                @moveItemDown="moveItemDown"
-                @moveItemPrevLevel="moveItemPrevLevel"
-                @moveItemNextLevel="moveItemNextLevel"
-                @childrenUpdated="$emit('childrenUpdated')"
-            />
-        </draggable>
-    </li>
-</template>
-
-<script>
-import draggable from 'vuedraggable';
-import { mapGetters, mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-tree-item',
-    components: {
-        draggable
-    },
-    props: {
-        element: {
-            type: Object,
-            required: true,
-        },
-        currentElement: {
-            type: Object,
-        },
-        depth: {
-            type: Number,
-            default: 0,
-        },
-        keyboardSelectedProp: {
-            type: Boolean,
-            default: false
-        },
-        newPos: {
-            type: Number
-        },
-        newParentId: {
-            type: Number
-        },
-        siblingCount: {
-            type: Number
-        }
-    },
-    data() {
-        return {
-            keyboardSelected: false
-        }
-    },
-    computed: {
-        ...mapGetters({
-            childrenById: 'courseware-structure/children',
-            structuralElementById: 'courseware-structural-elements/byId',
-            context: 'context',
-            taskById: 'courseware-tasks/byId',
-            userById: 'users/byId',
-            groupById: 'status-groups/byId',
-            viewMode: 'viewMode',
-            courseware: 'courseware',
-            progressData: 'progresses',
-            userIsTeacher: 'userIsTeacher',
-        }),
-        draggableData() {
-            return {
-                attrs: {
-                    role: 'listbox',
-                    ['aria-label']: this.$gettextInterpolate(this.$gettext('Unterseiten von %{elementName}'),{ elementName: this.element.attributes?.title })
-                }
-            };
-        },
-        children() {
-            if (!this.element) {
-                return [];
-            }
-
-            return this.childrenById(this.element.id)
-                .map((id) => this.structuralElementById({ id }))
-                .filter(Boolean).sort((a,b) => a.attributes.position - b.attributes.position);
-        },
-        nestedChildren() {
-            return this.element.nestedChildren ?? [];
-        },
-        hasChildren() {
-            return this.childrenById(this.element.id).length;
-        },
-        isRoot() {
-            return this.depth === 0;
-        },
-        isFirstLevel() {
-            return this.depth === 1;
-        },
-        isCurrent() {
-            return this.element.id === this.currentElement?.id;
-        },
-        hasReleaseOrWithdrawDate() {
-            return (
-                this.element.attributes?.['release-date'] !== null || this.element.attributes?.['withdraw-date'] !== null
-            );
-        },
-        hasWriteApproval() {
-            const writeApproval = this.element.attributes?.['write-approval'];
-
-            if (!writeApproval || Object.keys(writeApproval).length === 0) {
-                return false;
-            }
-            return (writeApproval.all || writeApproval.groups.length > 0 || writeApproval.users.length > 0) && this.element.attributes?.['can-edit'];
-        },
-        hasNoReadApproval() {
-            if (this.context.type === 'users') {
-                return false;
-            }
-            const readApproval = this.element.attributes?.['read-approval'];
-
-            if (!readApproval || Object.keys(readApproval).length === 0 || this.hasWriteApproval) {
-                return false;
-            }
-            return !readApproval.all && readApproval.groups.length === 0 && readApproval.users.length === 0;
-        },
-        hasPurposeClass() {
-            return this.purposeClass !== '';
-        },
-        purposeClass() {
-            if (
-                (this.isFirstLevel && this.context.type === 'users') ||
-                (this.context.type === 'courses' && this.element.attributes?.purpose === 'task')
-            ) {
-                return this.element.attributes?.purpose;
-            }
-            return '';
-        },
-        task() {
-            if (this.element.relationships?.task?.data) {
-                return this.taskById({
-                    id: this.element.relationships?.task?.data?.id,
-                });
-            }
-
-            return null;
-        },
-        taskProgress() {
-            return this.task ? this.task.attributes.progress + '%' : '';
-        },
-        solver() {
-            if (this.task) {
-                const solver = this.task.relationships.solver.data;
-                if (solver.type === 'users') {
-                    return this.userById({ id: solver.id });
-                }
-                if (solver.type === 'status-groups') {
-                    return this.groupById({ id: solver.id });
-                }
-            }
-
-            return null;
-        },
-        solverName() {
-            if (this.solver) {
-                if (this.solver.type === 'users') {
-                    return this.solver.attributes['formatted-name'];
-                }
-                if (this.solver.type === 'status-groups') {
-                    return this.solver.attributes.name;
-                }
-            }
-
-            return '';
-        },
-        isTask() {
-            return this.element.attributes?.purpose === 'task';
-        },
-        showItem() {
-            if (this.isTask) {
-                return this.task !== undefined;
-            }
-
-            return true;
-        },
-        editMode() {
-            return this.viewMode === 'edit';
-        },
-        dragOptions() {
-            return {
-                animation: 0,
-                disabled: false,
-                ghostClass: "cw-tree-item-ghost"
-            };
-        },
-        canEdit() {
-            return this.element.attributes['can-edit'];
-        },
-        inCourse() {
-            return this.context.type === 'courses';
-        },
-        progress() {
-            return this.progressData?.[this.element.id];
-        },
-        itemProgress() {
-            return this.progress?.progress?.self ?? 0;
-        },
-        complete() {
-            return this.itemProgress === 100;
-        },
-    },
-    methods: {
-        ...mapActions({
-            loadTask: 'loadTask',
-            setAssistiveLiveContents: 'setAssistiveLiveContents'
-        }),
-        endDrag(e) {
-            let sortArray = [];
-            for (let child of e.to.childNodes) {
-                sortArray.push({id: child.attributes.elementid.nodeValue, type: 'courseware-structural-elements'});
-            }
-
-            let data = {
-                id: e.item._underlying_vm_.id,
-                newPos: e.newIndex,
-                oldPos: e.oldIndex,
-                oldParent: e.item._underlying_vm_.relationships.parent.data.id,
-                newParent: e.to.__vue__.$attrs.elementId,
-                sortArray: sortArray
-            };
-
-            if (data.oldParent === data.newParent && data.oldPos === data.newPos) {
-                return;
-            }
-            if (data.oldParent !== data.newParent) {
-                sortArray.splice(data.newPos, 0, {id: data.id, type: 'courseware-structural-elements'});
-            } 
-
-            data.sortArray = sortArray;
-            this.$emit('sort', data);
-        },
-        sort(data) {
-            this.$emit('sort', data);
-        },
-        handleKeyEvent(e) {
-            switch (e.keyCode) {
-                case 13: // enter
-                    e.preventDefault();
-                    if (this.keyboardSelected) {
-                        this.storeKeyboardSorting();
-                    } else {
-                        this.keyboardSelected = true;
-                        const assistiveLive = 
-                            this.$gettextInterpolate(
-                                this.$gettext('%{elementTitle} ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen. Mit Pfeiltasten links und rechts kann die Position in der Hierarchie verändert werden.'),
-                                { elementTitle: this.element.attributes.title, pos: this.element.attributes.position + 1, listLength: this.siblingCount }
-                            );
-
-                            this.setAssistiveLiveContents(assistiveLive);
-                    }
-                    break;
-            }
-            if (this.keyboardSelected) {
-                const data = {
-                    element: this.element,
-                    parents: []
-                };
-                switch (e.keyCode) {
-                    case 27: // esc
-                    case 9: //tab
-                        this.abortKeyboardSorting();
-                        break;
-                    case 37: // left
-                        e.preventDefault();
-                        this.$emit('moveItemPrevLevel', data);
-                        break;
-                    case 38: // up
-                        e.preventDefault();    
-                        this.$emit('moveItemUp', data);
-                        break;
-                    case 39: // right
-                        e.preventDefault();
-                        this.$emit('moveItemNextLevel', data);
-                        break;
-                    case 40: // down
-                        e.preventDefault(); 
-                        this.$emit('moveItemDown', data);
-                        break;
-                }
-            }
-        },
-        moveItemPrevLevel(data) {
-            data.parents.push(this.element.id);
-            this.$emit('moveItemPrevLevel', data);
-        },
-        moveItemUp(data) {
-            data.parents.push(this.element.id);
-            this.$emit('moveItemUp', data);
-        },
-        moveItemNextLevel(data) {
-            data.parents.push(this.element.id);
-            this.$emit('moveItemNextLevel', data);
-        },
-        moveItemDown(data) {
-            data.parents.push(this.element.id);
-            this.$emit('moveItemDown', data);
-        },
-        abortKeyboardSorting() {
-            this.$emit('childrenUpdated');
-            const assistiveLive = this.$gettextInterpolate(
-                this.$gettext('%{elementTitle}. Neuordnung abgebrochen.'),
-                { elementTitle: this.element.attributes.title }
-            );
-            this.setAssistiveLiveContents(assistiveLive);
-            this.$nextTick(() => {
-                this.keyboardSelected = false;
-            });
-        },
-        storeKeyboardSorting() {
-            const data = {
-                id: this.element.id,
-                newPos: this.element.newPos,
-                oldPos: this.element.attributes.position,
-                oldParent: this.element.relationships.parent.data.id,
-                newParent: this.element.newParentId,
-                sortArray: this.element.sortArray
-            };
-            this.keyboardSelected = false;
-
-            if (data.newParent === undefined || data.newPos === undefined) {
-                const assistiveLive = this.$gettextInterpolate(
-                    this.$gettext('%{elementTitle}. Neuordnung nicht möglich.'),
-                    {elementTitle: this.element.attributes.title}
-                );
-                this.setAssistiveLiveContents(assistiveLive);
-                return;
-            }
-
-            if (data.oldParent === data.newParent && data.oldPos === data.newPos) {
-                const assistiveLive = this.$gettextInterpolate(
-                    this.$gettext('%{elementTitle}. Neuordnung abgebrochen.'),
-                    {elementTitle: this.element.attributes.title}
-                );
-                this.setAssistiveLiveContents(assistiveLive);
-                return;
-            }
-            this.$emit('sort', data);
-            const assistiveLive = this.$gettextInterpolate(
-                this.$gettext('%{elementTitle}, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.'), 
-                {elementTitle: this.element.attributes.title, pos: data.newPos + 1, listLength: this.siblingCount }
-            );
-            this.setAssistiveLiveContents(assistiveLive);
-        }
-    },
-    mounted() {
-        if (this.element.relationships?.task?.data) {
-            this.loadTask({
-                taskId: this.element.relationships.task.data.id,
-            });
-        }
-        if (this.newPos || this.newParentId) {
-            this.keyboardSelected = true;
-            this.$refs.sortableHandle.focus();
-        }
-    },
-    watch: {
-        newPos() {
-            this.keyboardSelected = true;
-            this.$refs.sortableHandle.focus();
-        },
-        newParentId() {
-            this.keyboardSelected = true;
-            this.$refs.sortableHandle.focus();
-        },
-    }
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareTypewriterBlock.vue b/resources/vue/components/courseware/CoursewareTypewriterBlock.vue
deleted file mode 100644
index 234f2aa..0000000
--- a/resources/vue/components/courseware/CoursewareTypewriterBlock.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-<template>
-    <div class="cw-block cw-block-typewriter">
-        <courseware-default-block
-        :block="block"
-        :canEdit="canEdit"
-        :isTeacher="isTeacher"
-        :preview="true"
-        @showEdit="initCurrentData"
-        @storeEdit="storeText"
-        @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div class="cw-typewriter-content">
-                    <vue-typer
-                        :text="currentText"
-                        initial-action="typing"
-                        :repeat="0"
-                        :type-delay="typeDelay"
-                        caret-animation="smooth"
-                        :class="[currentFont, currentSize]"
-                    ></vue-typer>
-                </div>
-            </template>
-            <template v-if="canEdit" #edit>
-                <form class="default" @submit.prevent="">
-                    <label class="col-4">
-                        {{ $gettext('Text') }}
-                        <textarea v-model="currentText" />
-                    </label>
-                    <br>
-                    <label class="col-1">
-                        {{ $gettext('Geschwindigkeit') }}
-                        <select v-model="currentSpeed" @change="restartTyping">
-                            <option value="0">{{ $gettext('Langsam') }}</option>
-                            <option value="1">{{ $gettext('Normal') }}</option>
-                            <option value="2">{{ $gettext('Schnell') }}</option>
-                            <option value="3">{{ $gettext('Sehr schnell') }}</option>
-                        </select>
-                    </label>
-                    <label class="col-1">
-                        {{ $gettext('Schriftart') }}
-                        <select v-model="currentFont">
-                            <option value="font-default">{{ $gettext('Standard') }}</option>
-                            <option value="font-typewriter">Lucida Sans Typewriter</option>
-                            <option value="font-trebuchet">Trebuchet MS</option>
-                            <option value="font-tahoma">Tahoma</option>
-                            <option value="font-georgia">Georgia</option>
-                            <option value="font-narrow">Arial Narrow</option>
-                        </select>
-                    </label>
-                    <label class="col-1">
-                        {{ $gettext('Schriftgröße') }}
-                        <select v-model="currentSize">
-                            <option value="size-default">100%</option>
-                            <option value="size-tall">125%</option>
-                            <option value="size-grande">150%</option>
-                            <option value="size-huge">200%</option>
-                        </select>
-                    </label>
-                </form>
-            </template>
-            <template #info>
-                <p>{{ $gettext('Informationen zum Schreibmaschinen-Block') }}</p>
-            </template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import { blockMixin } from './block-mixin.js';
-import { VueTyper } from 'vue-typer';
-import { mapActions } from 'vuex';
-
-export default {
-    name: 'courseware-typewriter-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        VueTyper,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            speeds: [200, 100, 50, 25],
-            typing: false,
-            speedClasses: [
-                'cw-typewriter-letter-fadein-slow',
-                'cw-typewriter-letter-fadein-normal',
-                'cw-typewriter-letter-fadein-fast',
-                'cw-typewriter-letter-fadein-veryfast',
-            ],
-            currentText: ' ',
-            currentSpeed: '',
-            currentFont: '',
-            currentSize: '',
-        };
-    },
-    computed: {
-        text() {
-            return this.block?.attributes?.payload?.text;
-        },
-        speed() {
-            return this.block?.attributes?.payload?.speed;
-        },
-        typeDelay() {
-            return this.speeds[this.currentSpeed];
-        },
-        font() {
-            return this.block?.attributes?.payload?.font;
-        },
-        size() {
-            return this.block?.attributes?.payload?.size;
-        }
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-        }),
-        initCurrentData() {
-            this.currentText = this.text;
-            this.currentSpeed = this.speed;
-            this.currentFont = this.font;
-            this.currentSize = this.size;
-        },
-        restartTyping() {
-            let text = this.currentText;
-            this.currentText = ' ';
-            this.$nextTick(()=> {
-                this.currentText = text;
-            });
-        },
-        storeText() {
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.text = this.currentText;
-            attributes.payload.speed = this.currentSpeed;
-            attributes.payload.font = this.currentFont;
-            attributes.payload.size = this.currentSize;
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        }
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareUnitItem.vue b/resources/vue/components/courseware/CoursewareUnitItem.vue
deleted file mode 100644
index 91b94a2..0000000
--- a/resources/vue/components/courseware/CoursewareUnitItem.vue
+++ /dev/null
@@ -1,197 +0,0 @@
-<template>
-    <li class="courseware-unit-item">
-        <courseware-tile
-            tag="div"
-            :color="color"
-            :title="title"
-            :descriptionLink="url"
-            :descriptionTitle="$gettext('Lernmaterial öffnen')"
-            :displayProgress="inCourseContext"
-            :progress="progress"
-            :imageUrl="imageUrl"
-        >
-            <template #image-overlay-with-action-menu>
-                <studip-action-menu
-                    class="cw-unit-action-menu"
-                    :items="menuItems"  
-                    :context="title"
-                    :collapseAt="0"
-                    @showDelete="openDeleteDialog"
-                    @showExport="openExportDialog"
-                    @showProgress="openProgressDialog"
-                    @showSettings="openSettingsDialog"
-                    @showLayout="openLayoutDialog"
-                    @copyUnit="copy"
-                />
-            </template>
-            <template #description>
-                {{ description }}
-            </template>
-        </courseware-tile>
-        <studip-dialog
-            v-if="showDeleteDialog"
-            :title="$gettext('Lernmaterial löschen')"
-            :question="$gettextInterpolate(
-                        $gettext('Möchten Sie das Lernmaterial %{ unitTitle } wirklich löschen?'),
-                        { unitTitle: title }
-                    )"
-            height="200"
-            @confirm="executeDelete"
-            @close="closeDeleteDialog"
-        ></studip-dialog>
-
-        <studip-dialog
-            v-if="showProgressDialog"
-            :title="userIsTeacher ? $gettext('Fortschritt aller Teilnehmenden') : $gettext('Mein Fortschritt')"
-            :closeText="$gettext('Schließen')"
-            closeClass="cancel"
-            width="800"
-            height="600"
-            @close="closeProgressDialog"
-        >
-            <template v-slot:dialogContent>
-                <courseware-unit-progress :progressData="progresses" :unitId="unit.id" :rootId="parseInt(unitElement.id)"/>
-            </template>
-        </studip-dialog>
-
-        <courseware-unit-item-dialog-export v-if="showExportDialog" :unit="unit" @close="showExportDialog = false" />
-        <courseware-unit-item-dialog-settings v-if="showSettingsDialog" :unit="unit" @close="closeSettingsDialog"/>
-        <courseware-unit-item-dialog-layout v-if="showLayoutDialog" :unitElement="unitElement" @close="closeLayoutDialog"/>
-    </li>
-</template>
-
-<script>
-import CoursewareTile from './CoursewareTile.vue';
-import CoursewareUnitItemDialogExport from './CoursewareUnitItemDialogExport.vue';
-import CoursewareUnitItemDialogSettings from './CoursewareUnitItemDialogSettings.vue';
-import CoursewareUnitItemDialogLayout from './CoursewareUnitItemDialogLayout.vue';
-import CoursewareUnitProgress from './CoursewareUnitProgress.vue';
-
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-unit-item',
-    components: {
-        CoursewareTile,
-        CoursewareUnitItemDialogExport,
-        CoursewareUnitItemDialogLayout,
-        CoursewareUnitItemDialogSettings,
-        CoursewareUnitProgress,
-    },
-    props: {
-        unit: Object,
-    },
-    data() {
-        return {
-            showDeleteDialog: false,
-            showExportDialog: false,
-            showSettingsDialog: false,
-            showProgressDialog: false,
-            showLayoutDialog: false,
-            progresses: null
-        }
-    },
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            structuralElementById: 'courseware-structural-elements/byId',
-            userIsTeacher: 'userIsTeacher'
-        }),
-        menuItems() {
-            let menu = [];
-            if (this.inCourseContext) {
-                menu.push({ id: 1, label: this.$gettext('Fortschritt'), icon: 'progress', emit: 'showProgress' });
-            }
-            if(this.userIsTeacher && this.inCourseContext) {
-                menu.push({ id: 2, label: this.$gettext('Einstellungen'), icon: 'settings', emit: 'showSettings' });
-            }
-            if(this.userIsTeacher || !this.inCourseContext) {
-                menu.push({ id: 4, label: this.$gettext('Darstellung'), icon: 'colorpicker', emit: 'showLayout' });
-                menu.push({ id: 4, label: this.$gettext('Duplizieren'), icon: 'copy', emit: 'copyUnit' });
-                menu.push({ id: 5, label: this.$gettext('Exportieren'), icon: 'export', emit: 'showExport' });
-                menu.push({ id: 6, label: this.$gettext('Löschen'), icon: 'trash', emit: 'showDelete' });
-            }
-
-            return menu;
-        },
-        unitElement() {
-            return this.structuralElementById({id: this.unit.relationships['structural-element'].data.id}) ?? null;
-        },
-        color() {
-            return this.unitElement?.attributes?.payload?.color ?? 'studip-blue';
-        },
-        title() {
-            return  this.unitElement?.attributes?.title ?? '';
-        },
-        description() {
-            return  this.unitElement?.attributes?.payload?.description ?? '';
-        },
-        imageUrl() {
-            return this.unitElement?.relationships?.image?.meta?.['download-url'] ?? '';
-        },
-        url() {
-            if (this.inCourseContext) {
-                return STUDIP.URLHelper.getURL('dispatch.php/course/courseware/courseware/' + this.unit.id , { cid: this.context.id });
-            } else {
-                return STUDIP.URLHelper.getURL('dispatch.php/contents/courseware/courseware/' + this.unit.id);
-            }
-        },
-        progress() {
-            if (this.unitElement) {
-                return this.progresses?.[this.unitElement.id]?.progress?.cumulative ?? 0;
-            }
-            return 0;
-        },
-        inCourseContext() {
-            return this.context.type === 'courses';
-        }
-    },
-    async mounted() {
-        if (this.inCourseContext) {
-            this.progresses = await this.loadUnitProgresses({unitId: this.unit.id});
-        }
-    },
-    methods: {
-        ...mapActions({
-            deleteUnit: 'deleteUnit',
-            loadUnitProgresses: 'loadUnitProgresses',
-            copyUnit: 'copyUnit',
-            companionSuccess: 'companionSuccess'
-        }),
-        executeDelete() {
-            this.deleteUnit({id: this.unit.id});
-        },
-        openDeleteDialog() {
-            this.showDeleteDialog = true;
-        },
-        closeDeleteDialog() {
-            this.showDeleteDialog = false;
-        },
-        openExportDialog() {
-            this.showExportDialog = true;
-        },
-        async openProgressDialog() {
-            this.showProgressDialog = true;
-            this.progresses = await this.loadUnitProgresses({unitId: this.unit.id});
-        },
-        closeProgressDialog() {
-            this.showProgressDialog = false;
-        },
-        openSettingsDialog() {
-            this.showSettingsDialog = true; 
-        },
-        closeSettingsDialog() {
-            this.showSettingsDialog = false;
-        },
-        openLayoutDialog() {
-            this.showLayoutDialog = true; 
-        },
-        closeLayoutDialog() {
-            this.showLayoutDialog = false;
-        },
-        async copy() {
-            await this.copyUnit({unitId: this.unit.id, modified: null});
-            this.companionSuccess({ info: this.$gettext('Lernmaterial kopiert.') });        }
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareUnitItemDialogExport.vue b/resources/vue/components/courseware/CoursewareUnitItemDialogExport.vue
deleted file mode 100644
index a9c4376..0000000
--- a/resources/vue/components/courseware/CoursewareUnitItemDialogExport.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<template>
-        <studip-dialog
-            :title="$gettext('Lernmaterial exportieren')"
-            :confirmText="$gettext('Exportieren')"
-            confirmClass="accept"
-            :closeText="$gettext('Schließen')"
-            closeClass="cancel"
-            height="350"
-            @close="$emit('close')"
-            @confirm="executeExport"
-        >
-            <template v-slot:dialogContent>
-                <courseware-companion-box
-                    v-show="!exportRunning"
-                    :msgCompanion="$gettextInterpolate($gettext('Export des Lernmaterials: %{title}'), {title: title})"
-                    mood="curious"
-                />
-
-                <courseware-companion-box
-                    v-show="exportRunning"
-                    :msgCompanion="$gettextInterpolate($gettext('%{title} wird exportiert, bitte haben sie einen Moment Geduld...'), {title: title})"
-                    mood="pointing"
-                />
-                <div v-show="exportRunning" class="cw-import-zip">
-                    <header>{{ exportState }}:</header>
-                    <div class="progress-bar-wrapper">
-                        <div
-                            class="progress-bar"
-                            role="progressbar"
-                            :style="{ width: exportProgress + '%' }"
-                            :aria-valuenow="exportProgress"
-                            aria-valuemin="0"
-                            aria-valuemax="100"
-                        >
-                            {{ exportProgress }}%
-                        </div>
-                    </div>
-                </div>
-            </template>
-        </studip-dialog>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareExport from '@/vue/mixins/courseware/export.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-unit-item-dialog-export',
-    mixins: [CoursewareExport],
-    components: {
-        CoursewareCompanionBox,
-    },
-    props: {
-        unit: Object
-    },
-    data() {
-        return {
-            currentInstance: null,
-            exportRunning: false,
-        }
-    },
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            exportProgress: 'exportProgress',
-            exportState: 'exportState',
-            instanceById: 'courseware-instances/byId',
-            structuralElementById: 'courseware-structural-elements/byId',
-            userIsTeacher: 'userIsTeacher', 
-        }),
-        instance() {
-            if (this.inCourseContext) {
-                return this.instanceById({id: 'course_' + this.context.id + '_' + this.unit.id});
-            } else {
-                return this.instanceById({id: 'user_' + this.context.id + '_' + this.unit.id});
-            }
-            
-        },
-        inCourseContext() {
-            return this.context.type === 'courses';
-        },
-        unitElement() {
-            return this.structuralElementById({id: this.unit.relationships['structural-element'].data.id}) ?? null;
-        },
-        title() {
-            return  this.unitElement?.attributes?.title ?? '';
-        },
-    },
-    methods: {
-        ...mapActions({
-            loadInstance: 'loadInstance',
-            setExportState: 'setExportState',
-            companionSuccess: 'companionSuccess'
-        }),
-        async loadUnitInstance() {
-            const context = {type: this.context.type, id: this.context.id, unit: this.unit.id};
-            await this.loadInstance(context);
-        },
-        async executeExport() {
-            if (this.exportRunning) {
-                return;
-            }
-
-            this.exportRunning = true;
-            
-            this.setExportState(this.$gettext('Lade Einstellungen'));
-            await this.loadUnitInstance();
-            this.setExportState('');
-
-            await this.sendExportZip(this.unitElement.id, {
-                withChildren: true,
-                completeExport: true,
-                settings: {
-                    'editing-permission-level': this.instance.attributes['editing-permission-level'] ?? 'tutor',
-                    'sequential-progression': this.instance.attributes['sequential-progression'] ?? 0,
-                    'certificate-settings': this.instance.attributes['certificate-settings'],
-                    'reminder-settings': this.instance.attributes['reminder-settings'],
-                    'reset-progress-settings': this.instance.attributes['reset-progress-settings']
-                }
-            });
-
-            this.exportRunning = false;
-            this.$emit('close');
-        },
-
-    },
-    async mounted() {
-        await this.loadUnitInstance();
-    }
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareUnitItemDialogLayout.vue b/resources/vue/components/courseware/CoursewareUnitItemDialogLayout.vue
deleted file mode 100644
index cfe9099..0000000
--- a/resources/vue/components/courseware/CoursewareUnitItemDialogLayout.vue
+++ /dev/null
@@ -1,197 +0,0 @@
-<template>
-        <studip-dialog
-        :title="$gettext('Darstellung')"
-        :confirmText="$gettext('Speichern')"
-        confirmClass="accept"
-        :closeText="$gettext('Schließen')"
-        closeClass="cancel"
-        height="720"
-        width="500"
-        @close="$emit('close')"
-        @confirm="storeLayout"
-    >
-        <template v-slot:dialogContent>
-            <form v-if="currentElement" class="default" @submit.prevent="">
-                <label>{{ $gettext('Vorschaubild') }}</label>
-                <img
-                    v-if="showPreviewImage"
-                    :src="image"
-                    class="cw-structural-element-image-preview"
-                    :alt="$gettext('Vorschaubild')"
-                />
-                <label v-if="showPreviewImage">
-                    <button class="button" @click="deleteImage">{{ $gettext('Bild löschen') }}</button>
-                </label>
-                <courseware-companion-box
-                    v-if="uploadFileError"
-                    :msgCompanion="uploadFileError"
-                    mood="sad"
-                />
-                <label v-if="!showPreviewImage">
-                    <img
-                        v-if="currentFile"
-                        :src="uploadImageURL"
-                        class="cw-structural-element-image-preview"
-                        :alt="$gettext('Vorschaubild')"
-                    />
-                    <div v-else class="cw-structural-element-image-preview-placeholder"></div>
-                    {{ $gettext('Bild hochladen') }}
-                    <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
-                </label>
-
-                <label>
-                    {{ $gettext('Titel') }}
-                    <input type="text" v-model="currentElement.attributes.title"/>
-                </label>
-                <label>
-                    {{ $gettext('Beschreibung') }}
-                    <textarea
-                        v-model="currentElement.attributes.payload.description"
-                        class="cw-structural-element-description"
-                    />
-                </label>
-                <label>
-                    {{ $gettext('Farbe') }}
-                    <studip-select
-                        v-model="currentElement.attributes.payload.color"
-                        :options="colors"
-                        :reduce="(color) => color.class"
-                        label="class"
-                        class="cw-vs-select"
-                    >
-                        <template #open-indicator="selectAttributes">
-                            <span v-bind="selectAttributes"
-                                ><studip-icon shape="arr_1down" :size="10"
-                            /></span>
-                        </template>
-                        <template #no-options>
-                            {{ $gettext('Es steht keine Auswahl zur Verfügung') }}.
-                        </template>
-                        <template #selected-option="{ name, hex }">
-                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                            ><span>{{ name }}</span>
-                        </template>
-                        <template #option="{ name, hex }">
-                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
-                            ><span>{{ name }}</span>
-                        </template>
-                    </studip-select>
-                </label>
-            </form>
-        </template>
-    </studip-dialog>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-
-import colorMixin from '@/vue/mixins/courseware/colors.js';
-import { mapActions, mapGetters } from 'vuex';
-
-
-export default {
-    name: 'courseware-unit-item-dialog-layout',
-    components: {
-        CoursewareCompanionBox
-    },
-    props: {
-        unitElement: Object
-    },
-    mixins: [colorMixin],
-    data() {
-        return {
-            currentElement: null,
-            deletingPreviewImage: false,
-            uploadFileError: '',
-            currentFile: null,
-            uploadImageURL: null,
-        }
-    },
-    computed: {
-        ...mapGetters({
-            userId: 'userId'
-        }),
-        colors() {
-            return this.mixinColors.filter(color => color.darkmode);
-        },
-        image() {
-            return this.currentElement.relationships?.image?.meta?.['download-url'] ?? null;
-        },
-
-        showPreviewImage() {
-            return this.image !== null && this.deletingPreviewImage === false;
-        },
-    },
-    methods: {
-        ...mapActions({
-            companionSuccess: 'companionSuccess',
-            companionWarning: 'companionWarning',
-            loadStructuralElement: 'loadStructuralElement',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-            updateStructuralElement: 'updateStructuralElement',
-            uploadImageForStructuralElement: 'uploadImageForStructuralElement',
-            deleteImageForStructuralElement: 'deleteImageForStructuralElement',
-        }),
-        initData() {
-            this.currentElement = _.cloneDeep(this.unitElement);
-        },
-        checkUploadFile() {
-            const file = this.$refs?.upload_image?.files[0];
-            if (file.size > 2097152) {
-                this.uploadFileError = this.$gettext('Diese Datei ist zu groß. Bitte wählen Sie eine kleinere Datei.');
-            } else if (!file.type.includes('image')) {
-                this.uploadFileError = this.$gettext('Diese Datei ist kein Bild. Bitte wählen Sie ein Bild aus.');
-            } else {
-                this.uploadFileError = '';
-                this.currentFile = file;
-                this.uploadImageURL = window.URL.createObjectURL(file);
-            }
-        },
-        deleteImage() {
-            if (!this.deletingPreviewImage) {
-                this.deletingPreviewImage = true;
-            }
-        },
-        async storeLayout() {
-            this.$emit('close');
-            await this.loadStructuralElement(this.currentElement.id);
-            if (
-                this.unitElement.relationships['edit-blocker'].data !== null
-                && this.unitElement.relationships['edit-blocker'].data?.id !== this.userId
-            ) {
-                this.companionWarning({
-                    info: this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, die Daten werden bereits von einem anderen Nutzer bearbeitet.')
-                });
-                return false;
-            } else {
-                await this.lockObject({ id: this.currentElement.id, type: 'courseware-structural-elements' });
-            }
-
-            if (this.currentFile) {
-                this.uploadImageForStructuralElement({
-                    structuralElement: this.currentElement,
-                    file: this.currentFile,
-                }).catch((error) => {
-                    console.error(error);
-                    this.companionWarning({
-                        info: this.$gettext('Beim Hochladen der Bilddatei ist ein Fehler aufgetretten.')
-                    });
-                });
-                await this.loadStructuralElement(this.currentElement.id);
-            } else if (this.deletingPreviewImage) {
-                await this.deleteImageForStructuralElement(this.currentElement);
-            }
-
-            await this.updateStructuralElement({
-                element: this.currentElement,
-                id: this.currentElement.id,
-            });
-            await this.unlockObject({ id: this.currentElement.id, type: 'courseware-structural-elements' });
-        }
-    },
-    async mounted() {
-        this.initData();
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareUnitItemDialogSettings.vue b/resources/vue/components/courseware/CoursewareUnitItemDialogSettings.vue
deleted file mode 100644
index 8438df3..0000000
--- a/resources/vue/components/courseware/CoursewareUnitItemDialogSettings.vue
+++ /dev/null
@@ -1,311 +0,0 @@
-<template>
-    <studip-dialog
-        :title="$gettext('Einstellungen')"
-        :confirmText="$gettext('Speichern')"
-        confirmClass="accept"
-        :closeText="$gettext('Schließen')"
-        closeClass="cancel"
-        height="600"
-        width="500"
-        @close="$emit('close')"
-        @confirm="storeSettings"
-    >
-        <template v-slot:dialogContent>
-            <form v-if="!loadSettings" class="default" @submit.prevent="">
-                <fieldset>
-                    <legend>{{ $gettext('Allgemeine Einstellungen') }}</legend>
-                    <label>
-                        <span>{{ $gettext('Art der Inhaltsabfolge') }}</span>
-                        <select class="size-s" v-model="currentProgression">
-                            <option value="0">{{ $gettext('Frei') }}</option>
-                            <option value="1">{{ $gettext('Sequentiell') }}</option>
-                        </select>
-                    </label>
-
-                    <label>
-                        <span>{{ $gettext('Editierberechtigung für Tutor/-innen') }}</span>
-                        <select class="size-s" v-model="currentPermissionLevel">
-                            <option value="dozent">{{ $gettext('Nein') }}</option>
-                            <option value="tutor">{{ $gettext('Ja') }}</option>
-                        </select>
-                    </label>
-                </fieldset>
-                <fieldset>
-                    <legend>{{ $gettext('Zertifikate') }}</legend>
-                    <label>
-                        <input type="checkbox" name="makecert" v-model="makeCert">
-                        <span>
-                            {{ $gettext('Zertifikat bei Erreichen einer Fortschrittsgrenze versenden') }}
-                        </span>
-                        <studip-tooltip-icon :text="$gettext('Erreicht eine Person in diesem Lernmaterial den ' +
-                            'hier eingestellten Fortschritt, so erhält Sie ein PDF-Zertifikat per E-Mail.')"/>
-                    </label>
-                    <label v-if="makeCert">
-                        <span>
-                            {{ $gettext('Erforderlicher Fortschritt (in Prozent), um ein Zertifikat zu erhalten') }}
-                        </span>
-                        <input type="number" min="1" max="100" name="threshold" v-model="certThreshold">
-                    </label>
-                    <label v-if="makeCert">
-                        <span>
-                            {{ $gettext('Hintergrundbild des Zertifikats wählen') }}
-                        </span>
-                        <courseware-file-chooser :isImage="true" v-model="certImage" @selectFile="updateCertImage" />
-                    </label>
-                </fieldset>
-                <fieldset>
-                    <legend>
-                        {{ $gettext('Erinnerungen') }}
-                    </legend>
-                    <label>
-                        <input type="checkbox" name="sendreminders" v-model="sendReminders">
-                        <span>
-                            {{ $gettext('Erinnerungsnachrichten an alle Teilnehmenden schicken') }}
-                        </span>
-                        <studip-tooltip-icon :text="$gettext('Hier können periodisch Nachrichten an alle ' +
-                        'Teilnehmenden verschickt werden, um z.B. an die Bearbeitung dieses Lernmaterials zu erinnern.')"/>
-                    </label>
-
-                    <label v-if="sendReminders">
-                        <span>
-                            {{ $gettext('Zeitraum zwischen Erinnerungen') }}
-                        </span>
-                        <select name="reminder_interval" v-model="reminderInterval">
-                            <option value="7">
-                                {{ $gettext('wöchentlich') }}
-                            </option>
-                            <option value="14">
-                                {{ $gettext('14-tägig') }}
-                            </option>
-                            <option value="30">
-                                {{ $gettext('monatlich') }}
-                            </option>
-                            <option value="90">
-                                {{ $gettext('vierteljährlich') }}
-                            </option>
-                            <option value="180">
-                                {{ $gettext('halbjährlich') }}
-                            </option>
-                            <option value="365">
-                                {{ $gettext('jährlich') }}
-                            </option>
-                        </select>
-                    </label>
-                    <label v-if="sendReminders" class="col-3">
-                        <span>
-                            {{ $gettext('Erstmalige Erinnerung am') }}
-                            <input type="date" name="reminder_start_date"
-                                v-model="reminderStartDate">
-                        </span>
-                    </label>
-                    <label v-if="sendReminders" class="col-3">
-                        <span>
-                            {{ $gettext('Letztmalige Erinnerung am') }}
-                            <input type="date" name="reminder_end_date"
-                                v-model="reminderEndDate">
-                        </span>
-                    </label>
-                    <label v-if="sendReminders">
-                        <span>
-                            {{ $gettext('Text der Erinnerungsmail') }}
-                            <textarea cols="70" rows="4" name="reminder_mail_text" data-editor="minimal"
-                                    v-model="reminderMailText"></textarea>
-                        </span>
-                    </label>
-                </fieldset>
-                <fieldset>
-                    <legend>
-                        {{ $gettext('Fortschritt') }}
-                    </legend>
-                    <label>
-                        <input type="checkbox" name="resetprogress" v-model="resetProgress">
-                        <span>
-                            {{ $gettext('Fortschritt periodisch auf 0 zurücksetzen') }}
-                        </span>
-                        <studip-tooltip-icon :text="$gettext('Hier kann eingestellt werden, den Fortschritt ' +
-                            'aller Teilnehmenden periodisch auf 0 zurückzusetzen.')"/>
-                    </label>
-                    <label v-if="resetProgress">
-                        <span>
-                            {{ $gettext('Zeitraum zum Rücksetzen des Fortschritts') }}
-                        </span>
-                        <select name="reset_progress_interval" v-model="resetProgressInterval">
-                            <option value="14">
-                                {{ $gettext('14-tägig') }}
-                            </option>
-                            <option value="30">
-                                {{ $gettext('monatlich') }}
-                            </option>
-                            <option value="90">
-                                {{ $gettext('vierteljährlich') }}
-                            </option>
-                            <option value="180">
-                                {{ $gettext('halbjährlich') }}
-                            </option>
-                            <option value="365">
-                                {{ $gettext('jährlich') }}
-                            </option>
-                        </select>
-                    </label>
-                    <label v-if="resetProgress" class="col-3">
-                        <span>
-                            {{ $gettext('Erstmaliges Zurücksetzen am') }}
-                            <input type="date" dataformatas="" name="reset_progress_start_date"
-                                v-model="resetProgressStartDate">
-                        </span>
-                    </label>
-                    <label v-if="resetProgress" class="col-3">
-                        <span>
-                            {{ $gettext('Letztmaliges Zurücksetzen am') }}
-                            <input type="date" name="reset_progress_end_date"
-                                v-model="resetProgressEndDate">
-                        </span>
-                    </label>
-                    <label v-if="resetProgress">
-                        <span>
-                            {{ $gettext('Text der Rücksetzungsmail') }}
-                            <textarea cols="70" rows="4" name="reset_progress_mail_text" data-editor="minimal"
-                                    v-model="resetProgressMailText"></textarea>
-                        </span>
-                    </label>
-                </fieldset>
-            </form>
-            <studip-progress-indicator v-else :description="$gettext('Lade Einstellungen…')"/>
-
-        </template>
-    </studip-dialog>
-</template>
-
-<script>
-import CoursewareFileChooser from "./CoursewareFileChooser.vue";
-import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-unit-item-dialog-settings',
-    components: {
-        CoursewareFileChooser,
-        StudipProgressIndicator,
-    },
-    props: {
-        unit: Object
-    },
-    data() {
-        return {
-            currentInstance: null,
-            loadSettings: false,
-            currentPermissionLevel: '',
-            currentProgression: 0,
-            makeCert: false,
-            certThreshold: 0,
-            certImage: '',
-            sendReminders: false,
-            reminderInterval: 7,
-            reminderStartDate: '',
-            reminderEndDate: '',
-            reminderMailText: '',
-            resetProgress: false,
-            resetProgressInterval: 180,
-            resetProgressStartDate: '',
-            resetProgressEndDate: '',
-            resetProgressMailText: ''
-        }
-    },
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            instanceById: 'courseware-instances/byId',
-            userIsTeacher: 'userIsTeacher'
-        }),
-        instance() {
-            if (this.inCourseContext) {
-                return this.instanceById({id: 'course_' + this.context.id + '_' + this.unit.id});
-            } else {
-                return this.instanceById({id: 'user_' + this.context.id + '_' + this.unit.id});
-            }
-            
-        },
-        inCourseContext() {
-            return this.context.type === 'courses';
-        }
-    },
-    methods: {
-        ...mapActions({
-            loadInstance: 'loadInstance',
-            storeCoursewareSettings: 'storeCoursewareSettings',
-            companionSuccess: 'companionSuccess'
-        }),
-        async loadUnitInstance() {
-            const context = {type: this.context.type, id: this.context.id, unit: this.unit.id};
-            await this.loadInstance(context);
-        },
-        initData() {
-            this.currentPermissionLevel = this.currentInstance.attributes['editing-permission-level'];
-            this.currentProgression = this.currentInstance.attributes['sequential-progression'] ? '1' : '0';
-            this.certSettings = this.currentInstance.attributes['certificate-settings'];
-            this.makeCert = typeof(this.certSettings) === 'object' &&
-                Object.keys(this.certSettings).length > 0;
-            this.certThreshold = this.certSettings.threshold;
-            this.certImage = this.certSettings.image;
-            this.reminderSettings = this.currentInstance.attributes['reminder-settings'];
-            this.sendReminders = typeof(this.reminderSettings) === 'object' &&
-                Object.keys(this.reminderSettings).length > 0;
-            this.reminderInterval = this.reminderSettings.interval;
-            this.reminderStartDate = this.reminderSettings.startDate;
-            this.reminderEndDate = this.reminderSettings.endDate;
-            this.reminderMailText = this.reminderSettings.mailText;
-            this.resetProgressSettings = this.currentInstance.attributes['reset-progress-settings'];
-            this.resetProgress = typeof(this.resetProgressSettings) === 'object' &&
-                Object.keys(this.resetProgressSettings).length > 0;
-            this.resetProgressInterval = this.resetProgressSettings.interval;
-            this.resetProgressStartDate = this.resetProgressSettings.startDate;
-            this.resetProgressEndDate = this.resetProgressSettings.endDate;
-            this.resetProgressMailText = this.resetProgressSettings.mailText;
-        },
-        storeSettings() {
-            this.$emit('close');
-            this.currentInstance.attributes['editing-permission-level'] = this.currentPermissionLevel;
-            this.currentInstance.attributes['sequential-progression'] = this.currentProgression;
-            this.currentInstance.attributes['certificate-settings'] = this.generateCertificateSettings();
-            this.currentInstance.attributes['reminder-settings'] = this.generateReminderSettings();
-            this.currentInstance.attributes['reset-progress-settings'] = this.generateResetProgressSettings();
-            this.storeCoursewareSettings({
-                instance: this.currentInstance,
-            });
-        },
-        generateCertificateSettings() {
-            return this.makeCert ? {
-                threshold: this.certThreshold,
-                image: this.certImage
-            } : {};
-        },
-        generateReminderSettings() {
-            return this.sendReminders ? {
-                interval: this.reminderInterval,
-                startDate: this.reminderStartDate,
-                endDate: this.reminderEndDate,
-                mailText: this.reminderMailText
-            } : {};
-        },
-        generateResetProgressSettings() {
-            return this.resetProgress ? {
-                interval: this.resetProgressInterval,
-                startDate: this.resetProgressStartDate,
-                endDate: this.resetProgressEndDate,
-                mailText: this.resetProgressMailText
-            } : {};
-        },
-        updateCertImage(file) {
-            this.certImage = file.id;
-        }
-    },
-    async mounted() {
-        this.loadSettings = true;
-        await this.loadUnitInstance();
-        this.loadSettings = false;
-        this.currentInstance = this.instance;
-        this.initData();
-    }
-}
-</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareUnitItems.vue b/resources/vue/components/courseware/CoursewareUnitItems.vue
deleted file mode 100644
index 8fd7c0f..0000000
--- a/resources/vue/components/courseware/CoursewareUnitItems.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<template>
-    <div class="cw-unit-items">
-        <h2 v-if="!inCourseContext && hasUnits">{{ $gettext('Persönliche Lernmaterialien') }}</h2>
-        <ul v-if="hasUnits" class="cw-tiles">
-            <courseware-unit-item v-for="unit in units" :key="unit.id" :unit="unit"/>
-        </ul>
-        <template v-if="!hasUnits && inCourseContext">
-            <div v-if="userIsTeacher" class="cw-contents-overview-teaser">
-                <div class="cw-contents-overview-teaser-content">
-                    <header>{{ $gettext('Lernmaterialien') }}</header>
-                    <p>
-                        {{ $gettext('Mit Courseware können Sie interaktive, multimediale Lerninhalte erstellen und nutzen. ' +
-                                    'Die Lerninhalte lassen sich hierarchisch unterteilen und können aus Texten, Videosequenzen, ' +
-                                    'Aufgaben, Kommunikationselementen und einer Vielzahl weiterer Elemente bestehen. ' +
-                                    'Fertige Lerninhalte können exportiert und in andere Kurse oder andere Installationen importiert werden. ' +
-                                    'Courseware ist nicht nur für digitale Formate geeignet, sondern kann auch genutzt werden, ' +
-                                    'um klassische Präsenzveranstaltungen mit Online-Anteilen zu ergänzen. Formate wie integriertes Lernen ' +
-                                    '(Blended Learning) lassen sich mit Courseware ideal umsetzen. Kollaboratives Lernen kann dank Schreibrechtevergabe ' +
-                                    'und dem Einsatz von Courseware in Studiengruppen realisiert werden.') }}
-                    </p>
-                    <button class="button" @click="setShowUnitAddDialog(true)">
-                        {{ $gettext('Neues Lernmaterial anlegen') }}
-                    </button>
-                </div>
-            </div>
-            <courseware-companion-box
-                v-else
-                :msgCompanion="$gettext('Es wurden leider noch keine Lernmaterialien angelegt.')"
-                mood="sad"
-            />
-        </template>
-        <div v-if="!hasUnits && !inCourseContext" class="cw-contents-overview-teaser">
-            <div class="cw-contents-overview-teaser-content">
-                <header>{{ $gettext('Ihre persönlichen Lernmaterialien') }}</header>
-                <p>{{ $gettext('Erstellen und verwalten Sie hier Ihre eigenen persönlichen Lernmaterialien in Form von ePorfolios, ' +
-                               'Vorlagen für Veranstaltungen oder einfach nur persönliche Inhalte für das Studium. ' +
-                               'Entwickeln Sie Ihre eigenen (Lehr-)Materialien für Studium oder die Lehre und teilen diese mit anderen Nutzenden.') }}</p>
-                <button class="button" @click="setShowUnitAddDialog(true)">
-                    {{ $gettext('Neues Lernmaterial anlegen') }}
-                </button>
-            </div>
-        </div>
-    </div>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareUnitItem from './CoursewareUnitItem.vue';
-
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-unit-items',
-    components: {
-        CoursewareCompanionBox,
-        CoursewareUnitItem,
-    },
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            coursewareUnits: 'courseware-units/all',
-            userIsTeacher: 'userIsTeacher'
-        }),
-        units() {
-            return this.coursewareUnits.filter(unit => unit.relationships.range.data.id === this.context.id) ?? [];
-        },
-        hasUnits() {
-            return this.units.length > 0;
-        },
-        inCourseContext() {
-            return this.context.type === 'courses';
-        }
-    },
-    methods: {
-        ...mapActions({
-            setShowUnitAddDialog: 'setShowUnitAddDialog',
-        }),
-    }
-}
-</script>
diff --git a/resources/vue/components/courseware/CoursewareUnitProgress.vue b/resources/vue/components/courseware/CoursewareUnitProgress.vue
deleted file mode 100644
index 6c7528f..0000000
--- a/resources/vue/components/courseware/CoursewareUnitProgress.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<template>
-    <div class="cw-unit-progress">
-        <p v-if="userIsTeacher">
-            {{
-                $gettext('Die Fortschrittsanzeige bezieht sich auf den Anteil der Teilnehmenden, die eine Seite aufgerufen haben.')
-            }}
-        </p>
-        <nav aria-label="Breadcrumb" class="cw-unit-progress-breadcrumb">
-            <a v-if="parent" href="#" :title="$gettext('Hauptseite')" @click="visitRoot">
-                <studip-icon shape="home" />
-            </a>
-            <a v-if="parent && parent.id !== rootId" href="#" :title="parent.name" @click="selectChapter(parent.id)">
-                | {{ parent.name }}
-            </a>
-        </nav>
-        <div v-if="selected" class="cw-unit-progress-chapter">
-            <h1>
-                <a
-                    :href="chapterUrl"
-                    :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name })"
-                >
-                    {{ selected.name }}
-                </a>
-            </h1>
-            <courseware-progress-circle
-                :title="selected.id === rootId ? $gettext('Gesamtes Lernmaterial') : $gettext('diese Seite inkl. darunter liegender Seiten')"
-                :value="parseInt(selected.progress.cumulative)"
-            />
-            <courseware-progress-circle
-                :title="$gettext('diese Seite')"
-                class="cw-unit-progress-current"
-                :value="parseInt(selected.progress.self)"
-            />
-        </div>
-        <div v-if="children.length > 0" class="cw-unit-progress-subchapter-list">
-            <courseware-unit-progress-item
-                v-for="chapter in children"
-                :key="chapter.id"
-                :name="chapter.name"
-                :value="chapter.progress.cumulative"
-                :chapterId="chapter.id"
-                @selectChapter="selectChapter"
-            />
-        </div>
-        <div v-else class="cw-unit-empty-info">
-            <courseware-companion-box
-                mood="sad"
-                :msgCompanion="$gettext('Diese Seite enthält keine darunter liegenden Seiten.')"
-            />
-        </div>
-    </div>
-</template>
-
-<script>
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareUnitProgressItem from './CoursewareUnitProgressItem.vue';
-import CoursewareProgressCircle from './CoursewareProgressCircle.vue';
-import StudipIcon from '../StudipIcon.vue';
-
-import { mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-unit-progress',
-    components: {
-        CoursewareCompanionBox,
-        CoursewareUnitProgressItem,
-        CoursewareProgressCircle,
-        StudipIcon,
-    },
-    props: {
-        progressData: Object,
-        unitId: String,
-        rootId: Number,
-    },
-    data() {
-        return {
-            selected: null,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            userIsTeacher: 'userIsTeacher',
-        }),
-        chapterUrl() {
-            return (
-                STUDIP.URLHelper.getURL('dispatch.php/course/courseware/courseware/' + this.unitId) +
-                '#/structural_element/' +
-                this.selected.id
-            );
-        },
-        parent() {
-            if (!this.selected?.parent_id) {
-                return null;
-            }
-
-            return this.progressData[this.selected.parent_id];
-        },
-        children() {
-            if (!this.selected) {
-                return [];
-            }
-
-            return Object.values(this.progressData).filter(({ parent_id }) => parent_id === this.selected.id);
-        },
-    },
-    methods: {
-        visitRoot() {
-            this.selected = this.progressData[this.rootId];
-        },
-        selectChapter(id) {
-            this.selected = this.progressData[id] ?? null;
-        },
-    },
-    mounted() {
-        this.visitRoot();
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareUnitProgressItem.vue b/resources/vue/components/courseware/CoursewareUnitProgressItem.vue
deleted file mode 100644
index b83a692..0000000
--- a/resources/vue/components/courseware/CoursewareUnitProgressItem.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-    <a 
-        href="#"
-        class="cw-unit-progress-item"
-        :title="name"
-        @click="$emit('selectChapter', chapterId)"
-    >
-        <div class="cw-unit-progress-item-value">
-            <courseware-progress-circle :value="parseInt(value)" />
-        </div>
-        <div class="cw-unit-progress-item-description">
-            {{ name }}
-        </div>
-    </a>
-</template>
-
-<script>
-import CoursewareProgressCircle from './CoursewareProgressCircle.vue';
-
-export default {
-    name: 'courseware-unit-progress-item',
-    components: {
-        CoursewareProgressCircle,
-    },
-    props: {
-        name: String,
-        value: Number,
-        chapterId: Number,
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareVideoBlock.vue b/resources/vue/components/courseware/CoursewareVideoBlock.vue
deleted file mode 100644
index 66c3cf2..0000000
--- a/resources/vue/components/courseware/CoursewareVideoBlock.vue
+++ /dev/null
@@ -1,242 +0,0 @@
-<template>
-    <div class="cw-block cw-block-video">
-        <courseware-default-block
-            :block="block"
-            :canEdit="canEdit"
-            :isTeacher="isTeacher"
-            :preview="true"
-            @showEdit="initCurrentData"
-            @storeEdit="storeBlock"
-            @closeEdit="initCurrentData"
-        >
-            <template #content>
-                <div v-if="currentTitle !== '' && currentURL" class="cw-block-title">{{ currentTitle }}</div>
-                <video
-                    v-show="currentURL"
-                    :src="currentURL"
-                    :type="currentFile !== '' ? currentFile.mime_type : ''"
-                    controls
-                    :autoplay="currentAutoplay === 'enabled'"
-                    @contextmenu="contextHandler"
-                />
-            </template>
-            <template v-if="canEdit" #edit>
-                <courseware-tabs>
-                    <courseware-tab
-                        :index="0"
-                        :name="$gettext('Grunddaten')"
-                        :selected="true"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Überschrift') }}
-                                <input type="text" v-model="currentTitle" />
-                            </label>
-                            <label>
-                                {{ $gettext('Quelle') }}
-                                <select v-model="currentSource">
-                                    <option value="studip">{{ $gettext('Dateibereich') }}</option>
-                                    <option value="web">{{ $gettext('Web-Adresse') }}</option>
-                                </select>
-                            </label>
-                            <label v-show="currentSource === 'web'">
-                                {{ $gettext('URL') }}
-                                <input type="text" v-model="currentWebUrl" />
-                            </label>
-                            <label v-show="currentSource === 'studip'">
-                                {{ $gettext('Datei') }}
-                                <courseware-file-chooser
-                                    v-model="currentFileId"
-                                    :isVideo="true"
-                                    @selectFile="updateCurrentFile"
-                                />
-                            </label>
-
-                        </form>
-                    </courseware-tab>
-                    <courseware-tab
-                        :index="1"
-                        :name="$gettext('Video Einstellungen')"
-                    >
-                        <form class="default" @submit.prevent="">
-                            <label>
-                                {{ $gettext('Seitenverhältnis') }}
-                                <select v-model="currentAspect">
-                                    <option value="169">16:9</option>
-                                    <option value="43">4:3</option>
-                                </select>
-                            </label>
-                            <label>
-                                {{ $gettext('Video startet automatisch') }}
-                                <select v-model="currentAutoplay">
-                                    <option value="disabled">{{ $gettext('Nein') }}</option>
-                                    <option value="enabled">{{ $gettext('Ja') }}</option>
-                                </select>
-                            </label>
-                            <label>
-                                {{ $gettext('Contextmenü') }}
-                                <select v-model="currentContextMenu">
-                                    <option value="enabled">{{ $gettext('Erlauben') }}</option>
-                                    <option value="disabled">{{ $gettext('Verhindern') }}</option>
-                                </select>
-                            </label>
-                        </form>
-                    </courseware-tab>
-                </courseware-tabs>
-            </template>
-            <template #info>{{ $gettext('Informationen zum Video-Block') }}</template>
-        </courseware-default-block>
-    </div>
-</template>
-
-<script>
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareFileChooser from './CoursewareFileChooser.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-import { blockMixin } from './block-mixin.js';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-video-block',
-    mixins: [blockMixin],
-    components: {
-        CoursewareDefaultBlock,
-        CoursewareFileChooser,
-        CoursewareTabs,
-        CoursewareTab,
-    },
-    props: {
-        block: Object,
-        canEdit: Boolean,
-        isTeacher: Boolean,
-    },
-    data() {
-        return {
-            currentSource: '',
-            currentTitle: '',
-            currentFile: {},
-            currentFileId: '',
-            currentAspect: '',
-            currentContextMenu: '',
-            currentAutoplay: '',
-            currentWebUrl: '',
-        };
-    },
-    computed: {
-        ...mapGetters({
-            fileRefById: 'file-refs/byId',
-            urlHelper: 'urlHelper',
-        }),
-        title() {
-            return this.block?.attributes?.payload?.title;
-        },
-        source() {
-            return this.block?.attributes?.payload?.source;
-        },
-        fileId() {
-            return this.block?.attributes?.payload?.file_id;
-        },
-        webUrl() {
-            return this.block?.attributes?.payload?.web_url;
-        },
-        aspect() {
-            return this.block?.attributes?.payload?.aspect;
-        },
-        contextMenu() {
-            return this.block?.attributes?.payload?.context_menu;
-        },
-        autoplay() {
-            return this.block?.attributes?.payload?.autoplay;
-        },
-        currentURL() {
-            if (this.currentSource === 'studip' && this.currentFile) {
-                return this.currentFile.download_url;
-            }
-            if (this.currentSource === 'web') {
-                return this.currentWebUrl;
-            }
-            return false;
-        },
-
-    },
-    mounted() {
-        this.initCurrentData();
-    },
-    methods: {
-        ...mapActions({
-            updateBlock: 'updateBlockInContainer',
-            loadFileRef: 'file-refs/loadById',
-            companionWarning: 'companionWarning',
-        }),
-        storeBlock() {
-            let cmpInfo = false;
-            let attributes = {};
-            attributes.payload = {};
-            attributes.payload.title = this.currentTitle;
-            attributes.payload.source = this.currentSource;
-            if (this.currentSource === 'studip' && this.currentFile !== undefined && this.currentFileId !== '') {
-                attributes.payload.file_id = this.currentFile.id;
-                attributes.payload.web_url = '';
-            } else if (this.currentSource === 'web' && this.currentWebUrl !== '') {
-                attributes.payload.file_id = '';
-                attributes.payload.web_url = this.currentWebUrl;
-            } else {
-                cmpInfo = this.$gettext('Bitte wählen Sie ein Video aus.');
-            }
-            attributes.payload.aspect = this.currentAspect;
-            attributes.payload.context_menu = this.currentContextMenu;
-            attributes.payload.autoplay = this.currentAutoplay;
-
-            if (cmpInfo) {
-                this.companionWarning({ info: cmpInfo });
-                return false;
-            }
-
-            this.updateBlock({
-                attributes: attributes,
-                blockId: this.block.id,
-                containerId: this.block.relationships.container.data.id,
-            });
-        },
-        async initCurrentData() {
-            this.currentSource = this.source;
-            this.currentTitle = this.title;
-            this.currentWebUrl = this.webUrl;
-            this.currentFileId = this.fileId;
-            this.currentAspect = this.aspect;
-            this.currentContextMenu = this.contextMenu;
-            this.currentAutoplay = this.autoplay;
-            if (this.fileId !== '') {
-                await this.loadFile();
-            }
-        },
-        async loadFile() {
-            const id = this.currentFileId;
-            await this.loadFileRef({ id });
-            const fileRef = this.fileRefById({ id });
-
-            if (fileRef) {
-                this.updateCurrentFile({
-                    id: fileRef.id,
-                    name: fileRef.attributes.name,
-                    download_url: this.urlHelper.getURL(
-                        'sendfile.php',
-                        { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
-                        true
-                    ),
-                });
-            }
-        },
-        updateCurrentFile(file) {
-            this.currentFile = file;
-            this.currentFileId = file.id;
-        },
-        contextHandler(e) {
-            if (this.currentContextMenu === 'disabled') {
-                e.preventDefault();
-            }
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareViewWidget.vue b/resources/vue/components/courseware/CoursewareViewWidget.vue
deleted file mode 100644
index f6ab44c..0000000
--- a/resources/vue/components/courseware/CoursewareViewWidget.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<template>
-    <sidebar-widget :title="$gettext('Ansichten')" v-if="structuralElement">
-        <template #content>
-            <ul class="widget-list widget-links sidebar-views cw-view-widget">
-                <li :class="{ active: readView }">
-                    <button @click="setReadView">
-                        <translate>Lesen</translate>
-                    </button>
-                </li>
-                <li
-                    v-if="canEdit"
-                    :class="{ active: editView }"
-                >
-                    <button @click="setEditView">
-                        <translate>Bearbeiten</translate>
-                    </button>
-                </li>
-                <li
-                    v-if="context.type === 'courses' && canVisit"
-                    :class="{ active: discussView }"
-                >
-                    <button @click="setDiscussView">
-                        <translate>Kommentieren</translate>
-                    </button>
-                </li>
-            </ul>
-        </template>
-    </sidebar-widget>
-</template>
-
-<script>
-import SidebarWidget from '../SidebarWidget.vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-view-widget',
-    props: ['structuralElement', 'canVisit'],
-    components: {
-        SidebarWidget
-    },
-    computed: {
-        ...mapGetters({
-            viewMode: 'viewMode',
-            context: 'context',
-        }),
-        readView() {
-            return this.viewMode === 'read';
-        },
-        editView() {
-            return this.viewMode === 'edit';
-        },
-        discussView() {
-            return this.viewMode === 'discuss';
-        },
-        canEdit() {
-            if (!this.structuralElement) {
-                return false;
-            }
-            return this.structuralElement.attributes['can-edit'];
-        },
-    },
-    methods: {
-        ...mapActions({
-            coursewareViewMode: 'coursewareViewMode',
-            coursewareBlockAdder: 'coursewareBlockAdder',
-            setToolbarItem: 'coursewareSelectedToolbarItem',
-        }),
-        setReadView() {
-            this.coursewareViewMode('read');
-            this.setToolbarItem('contents');
-            this.coursewareBlockAdder({});
-        },
-        setEditView() {
-            this.coursewareViewMode('edit');
-        },
-        setDiscussView() {
-            this.coursewareViewMode('discuss');
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/CoursewareWelcomeScreen.vue b/resources/vue/components/courseware/CoursewareWelcomeScreen.vue
deleted file mode 100644
index 5081368..0000000
--- a/resources/vue/components/courseware/CoursewareWelcomeScreen.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<template>
-    <div class="cw-welcome-screen">
-        <div class="cw-welcome-screen-keyvisual"></div>
-        <header>
-            {{ $gettext('Willkommen bei Courseware') }}
-        </header>
-        <div class="cw-welcome-screen-actions">
-            <a href="https://hilfe.studip.de/help/5.0/de/Basis.Courseware" target="_blank" class="button">
-                {{ $gettext('Mehr über Courseware erfahren') }}
-            </a>
-            <button class="button" :title="$gettext('Fügt einen Standard-Abschnitt mit einem Text-Block hinzu')" @click="addDefault">
-                {{ $gettext('Ersten Inhalt erstellen') }}
-            </button>
-            <button class="button" @click="addContainer">
-                {{ $gettext('Einen Abschnitt auswählen') }}
-            </button>
-        </div>
-    </div>
-</template>
-
-<script>
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-welcome-screen',
-    computed: {
-        ...mapGetters({
-            consumeMode: 'consumeMode',
-            lastCreatedBlocks: 'courseware-blocks/lastCreated',
-            lastCreatedContainers: 'courseware-containers/lastCreated'
-        }),
-    },
-    methods: {
-        ...mapActions({
-            createContainer: 'createContainer',
-            createBlock: 'createBlockInContainer',
-            coursewareBlockAdder: 'coursewareBlockAdder',
-            companionSuccess: 'companionSuccess',
-            updateContainer: 'updateContainer',
-            lockObject: 'lockObject',
-            unlockObject: 'unlockObject',
-
-            coursewareConsumeMode: 'coursewareConsumeMode',
-            coursewareViewMode: 'coursewareViewMode',
-            coursewareContainerAdder: 'coursewareContainerAdder',
-            coursewareShowToolbar: 'coursewareShowToolbar'
-
-        }),
-        addContainer() {
-            this.coursewareConsumeMode(false);
-            this.coursewareShowToolbar(true);
-            this.coursewareViewMode('edit');
-            this.$nextTick(() => {
-                this.coursewareContainerAdder(true);
-            });
-        },
-        async addDefault() {
-            let attributes = {};
-            attributes["container-type"] = 'list';
-            attributes.payload = {
-                colspan: 'full',
-                sections: [{ name: 'Liste', icon: '', blocks: [] }],
-            };
-            await this.createContainer({ structuralElementId: this.$route.params.id, attributes: attributes });
-            let newContainer = this.lastCreatedContainers;
-            await this.lockObject({ id: newContainer.id, type: 'courseware-containers' });
-            await this.createBlock({
-                container: newContainer,
-                section: 0,
-                blockType: 'text',
-            });
-            this.coursewareViewMode('edit');
-            this.coursewareConsumeMode(false);
-            this.companionSuccess({
-                info: this.$gettext('Das Elemente für Ihren ersten Inhalt wurde angelegt.'),
-            });
-            const newBlock = this.lastCreatedBlocks;
-            newContainer.attributes.payload.sections[0].blocks.push(newBlock.id);
-            const structuralElementId = this.$route.params.id
-            await this.updateContainer({ container: newContainer, structuralElementId: structuralElementId });
-            await this.unlockObject({ id: newContainer.id, type: 'courseware-containers' });
-            
-
-        }
-    }
-
-}
-</script>
diff --git a/resources/vue/components/courseware/IndexApp.vue b/resources/vue/components/courseware/IndexApp.vue
index 0bcf2f2..cbeeb4c 100644
--- a/resources/vue/components/courseware/IndexApp.vue
+++ b/resources/vue/components/courseware/IndexApp.vue
@@ -27,7 +27,7 @@
         </div>
         <studip-progress-indicator
             v-if="structureLoadingState === 'loading'"
-            class="cw-loading-indicator-content"
+            class="loading-indicator-content"
             :description="$gettext('Lade Lernmaterial...')"
         />
         <courseware-companion-box
@@ -40,15 +40,16 @@
 </template>
 
 <script>
-import CoursewareStructuralElement from './CoursewareStructuralElement.vue';
-import CoursewareSearchResults from './CoursewareSearchResults.vue';
-import CoursewareViewWidget from './CoursewareViewWidget.vue';
-import CoursewareActionWidget from './CoursewareActionWidget.vue';
-import CoursewareExportWidget from './CoursewareExportWidget.vue';
-import CoursewareImportWidget from './CoursewareImportWidget.vue';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareSearchWidget from './CoursewareSearchWidget.vue';
-import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
+import CoursewareStructuralElement from './structural-element/CoursewareStructuralElement.vue';
+import CoursewareSearchResults from './structural-element/CoursewareSearchResults.vue';
+import CoursewareCompanionBox from './layouts/CoursewareCompanionBox.vue';
+import CoursewareCompanionOverlay from './layouts/CoursewareCompanionOverlay.vue';
+import CoursewareViewWidget from './widgets/CoursewareViewWidget.vue';
+import CoursewareActionWidget from './widgets/CoursewareActionWidget.vue';
+import CoursewareExportWidget from './widgets/CoursewareExportWidget.vue';
+import CoursewareImportWidget from './widgets/CoursewareImportWidget.vue';
+import CoursewareSearchWidget from './widgets/CoursewareSearchWidget.vue';
+
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
 
 import { mapActions, mapGetters } from 'vuex';
diff --git a/resources/vue/components/courseware/IsoDate.vue b/resources/vue/components/courseware/IsoDate.vue
deleted file mode 100644
index 4ab6840..0000000
--- a/resources/vue/components/courseware/IsoDate.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-<template>
-    <studip-date-time :timestamp="unixTimestamp" />
-</template>
-
-<script>
-export default {
-    name: 'courseware-iso-date',
-    props: {
-        date: String,
-    },
-    computed: {
-        unixTimestamp() {
-            return +new Date(this.date) / 1000;
-        },
-    },
-};
-</script>
diff --git a/resources/vue/components/courseware/PublicApp.vue b/resources/vue/components/courseware/PublicApp.vue
index 616db7f..0a082f0 100644
--- a/resources/vue/components/courseware/PublicApp.vue
+++ b/resources/vue/components/courseware/PublicApp.vue
@@ -10,7 +10,7 @@
         </div>
         <studip-progress-indicator
             v-if="structureLoadingState === 'loading'"
-            class="cw-loading-indicator-content"
+            class="loading-indicator-content"
             :description="$gettext('Lade Lernmaterial...')"
         />
         <courseware-companion-box
@@ -36,8 +36,8 @@
 </template>
 
 <script>
-import PublicCoursewareStructuralElement from './PublicCoursewareStructuralElement.vue';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+import PublicCoursewareStructuralElement from './structural-element/PublicCoursewareStructuralElement.vue';
+import CoursewareCompanionBox from './layouts/CoursewareCompanionBox.vue';
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/PublicCoursewareStructuralElement.vue b/resources/vue/components/courseware/PublicCoursewareStructuralElement.vue
deleted file mode 100644
index f6956d3..0000000
--- a/resources/vue/components/courseware/PublicCoursewareStructuralElement.vue
+++ /dev/null
@@ -1,256 +0,0 @@
-<template>
-    <focus-trap v-model="consumModeTrap">
-        <div
-            :class="{ 'cw-structural-element-consumemode': consumeMode }"
-            class="cw-structural-element"
-        >
-            <div v-if="structuralElement" class="cw-structural-element-content">
-                <courseware-ribbon :canEdit="false" :disableSettings="true" :disableAdder="true">
-                    <template #buttons>
-                        <router-link v-if="prevElement" :to="'/structural_element/' + prevElement.id">
-                            <div class="cw-ribbon-button cw-ribbon-button-prev" :title="textRibbon.perv" />
-                        </router-link>
-                        <div v-else class="cw-ribbon-button cw-ribbon-button-prev-disabled" :title="$gettext('keine vorherige Seite')"/>
-                        <router-link v-if="nextElement" :to="'/structural_element/' + nextElement.id">
-                            <div class="cw-ribbon-button cw-ribbon-button-next" :title="textRibbon.next" />
-                        </router-link>
-                        <div v-else class="cw-ribbon-button cw-ribbon-button-next-disabled" :title="$gettext('keine nächste Seite')"/>
-                    </template>
-                    <template #breadcrumbList>
-                        <li
-                            v-for="ancestor in ancestors"
-                            :key="ancestor.id"
-                            :title="ancestor.attributes.title"
-                            class="cw-ribbon-breadcrumb-item"
-                        >
-                            <span>
-                                <router-link :to="'/structural_element/' + ancestor.id">{{ ancestor.attributes.title || "–" }}</router-link>
-                            </span>
-                        </li>
-                        <li
-                            class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current"
-                            :title="structuralElement.attributes.title"
-                        >
-                            <span>{{ structuralElement.attributes.title || "–" }}</span>
-                        </li>
-                    </template>
-                    <template #breadcrumbFallback>
-                        <li
-                            class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current"
-                            :title="structuralElement.attributes.title"
-                        >
-                            <span>{{ structuralElement.attributes.title }}</span>
-                        </li>
-                    </template>
-                </courseware-ribbon>
-
-                <div
-                    class="cw-container-wrapper"
-                    :class="{
-                        'cw-container-wrapper-consume': consumeMode,
-                    }"
-                >
-                    <div v-if="structuralElementLoaded" class="cw-companion-box-wrapper">
-                        <courseware-empty-element-box
-                            v-if="noContainers"
-                            :noContainers="noContainers"
-                        />
-                    </div>
-                    <component
-                        v-for="container in containers"
-                        :key="container.id"
-                        :is="containerComponent(container)"
-                        :container="container"
-                        :canEdit="false"
-                        :canAddElements="false"
-                        :isTeacher="false"
-                        class="cw-container-item"
-                    />
-                </div>
-            </div>
-        </div>
-    </focus-trap>
-</template>
-
-<script>
-import ContainerComponents from './container-components.js';
-import CoursewarePluginComponents from './plugin-components.js';
-import CoursewareAccordionContainer from './CoursewareAccordionContainer.vue';
-import CoursewareListContainer from './CoursewareListContainer.vue';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
-import CoursewareEmptyElementBox from './CoursewareEmptyElementBox.vue';
-import CoursewareTabsContainer from './CoursewareTabsContainer.vue';
-import CoursewareRibbon from './CoursewareRibbon.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
-
-import IsoDate from './IsoDate.vue';
-import { FocusTrap } from 'focus-trap-vue';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'public-courseware-structural-element',
-
-    components: {
-        ContainerComponents,
-        CoursewareRibbon,
-        CoursewareListContainer,
-        CoursewareAccordionContainer,
-        CoursewareTabsContainer,
-        CoursewareCompanionBox,
-        CoursewareCompanionOverlay,
-        CoursewareEmptyElementBox,
-        CoursewareTabs,
-        CoursewareTab,
-        FocusTrap,
-        IsoDate,
-    },
-
-    props: ['orderedStructuralElements', 'structuralElement'],
-
-    data() {
-        return {
-            consumModeTrap: false,
-            textRibbon: {
-                perv: this.$gettext('zurück'),
-                next: this.$gettext('weiter'),
-            },
-        }
-    },
-
-    computed: {
-        ...mapGetters({
-            courseware: 'courseware',
-            context: 'context',
-            consumeMode: 'consumeMode',
-            containerById: 'courseware-containers/byId',
-            pluginManager: 'pluginManager',
-            relatedContainers: 'courseware-containers/related',
-            relatedStructuralElements: 'courseware-structural-elements/related',
-            structuralElementById: 'courseware-structural-elements/byId',
-        }),
-
-        currentId() {
-            return this.structuralElement?.id;
-        },
-
-        image() {
-            return this.structuralElement.relationships?.image?.meta?.['download-url'] ?? null;
-        },
-
-        structuralElementLoaded() {
-            return this.structuralElement !== null && this.structuralElement !== {};
-        },
-
-        ancestors() {
-            if (!this.structuralElement) {
-                return [];
-            }
-
-            const finder = (parent) => {
-                const parentId = parent.relationships?.parent?.data?.id;
-                if (!parentId) {
-                    return null;
-                }
-                const element = this.structuralElementById({ id: parentId });
-
-                return element;
-            };
-
-            const visitAncestors = function* (node) {
-                const parent = finder(node);
-                if (parent) {
-                    yield parent;
-                    yield* visitAncestors(parent);
-                }
-            };
-
-            return [...visitAncestors(this.structuralElement)].reverse();
-        },
-
-        prevElement() {
-            const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id);
-            if (currentIndex <= 0) {
-                return null;
-            }
-            const previousId = this.orderedStructuralElements[currentIndex - 1];
-            const previous = this.structuralElementById({ id: previousId });
-
-            return previous;
-        },
-
-        nextElement() {
-            const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id);
-            const lastIndex = this.orderedStructuralElements.length - 1;
-            if (currentIndex === -1 || currentIndex === lastIndex) {
-                return null;
-            }
-            const nextId = this.orderedStructuralElements[currentIndex + 1];
-            const next = this.structuralElementById({ id: nextId });
-
-            return next;
-        },
-
-        empty() {
-            if (this.containers === null) {
-                return true;
-            } else {
-                return !this.containers.some((container) => container.relationships.blocks.data.length > 0);
-            }
-        },
-
-        containers() {
-            let containers = [];
-            let relatedContainers = this.structuralElement?.relationships?.containers?.data;
-
-            if (relatedContainers) {
-                for (const container of relatedContainers) {
-                    containers.push(this.containerById({ id: container.id}));
-                }
-            }
-
-            return containers;
-        },
-
-        noContainers() {
-            if (this.containers === null) {
-                return true;
-            } else {
-                return this.containers.length === 0;
-            }
-        },
-
-        isRoot() {
-            return this.structuralElement.id === this.context.rootId;
-        },
-    },
-
-    methods: {
-        ...mapActions({
-            companionError: 'companionError',
-            companionInfo: 'companionInfo',
-            companionSuccess: 'companionSuccess',
-        }),
-        containerComponent(container) {
-            return 'courseware-' + container.attributes['container-type'] + '-container';
-        },
-    },
-
-    created() {
-        this.pluginManager.registerComponentsLocally(this);
-    },
-
-    watch: {
-        consumeMode(newState) {
-            this.consumModeTrap = newState;
-        },
-    },
-
-    // this line provides all the components to courseware plugins
-    provide: () => ({
-        containerComponents: ContainerComponents,
-        coursewarePluginComponents: CoursewarePluginComponents,
-    }),
-};
-</script>
diff --git a/resources/vue/components/courseware/ShelfApp.vue b/resources/vue/components/courseware/ShelfApp.vue
index d9b37a4..7f0f274 100644
--- a/resources/vue/components/courseware/ShelfApp.vue
+++ b/resources/vue/components/courseware/ShelfApp.vue
@@ -18,14 +18,14 @@
 </template>
 
 <script>
-import CoursewareShelfActionWidget from './CoursewareShelfActionWidget.vue';
-import CoursewareShelfImportWidget from './CoursewareShelfImportWidget.vue';
-import CoursewareShelfDialogAdd from './CoursewareShelfDialogAdd.vue';
-import CoursewareShelfDialogCopy from './CoursewareShelfDialogCopy.vue';
-import CoursewareShelfDialogImport from './CoursewareShelfDialogImport.vue';
-import CoursewareUnitItems from './CoursewareUnitItems.vue';
-import CoursewareSharedItems from './CoursewareSharedItems.vue';
-import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
+import CoursewareShelfActionWidget from './widgets/CoursewareShelfActionWidget.vue';
+import CoursewareShelfImportWidget from './widgets/CoursewareShelfImportWidget.vue';
+import CoursewareShelfDialogAdd from './unit/CoursewareShelfDialogAdd.vue';
+import CoursewareShelfDialogCopy from './unit/CoursewareShelfDialogCopy.vue';
+import CoursewareShelfDialogImport from './unit/CoursewareShelfDialogImport.vue';
+import CoursewareUnitItems from './unit/CoursewareUnitItems.vue';
+import CoursewareSharedItems from './unit/CoursewareSharedItems.vue';
+import CoursewareCompanionOverlay from './layouts/CoursewareCompanionOverlay.vue';
 
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/TasksApp.vue b/resources/vue/components/courseware/TasksApp.vue
index 7c149b8..8a406d8 100644
--- a/resources/vue/components/courseware/TasksApp.vue
+++ b/resources/vue/components/courseware/TasksApp.vue
@@ -12,10 +12,10 @@
 </template>
 
 <script>
-import CoursewareTasksActionWidget from './CoursewareTasksActionWidget.vue';
+import CoursewareTasksActionWidget from './widgets/CoursewareTasksActionWidget.vue';
 import CoursewareDashboardTasks from './CoursewareDashboardTasks.vue';
 import CoursewareDashboardStudents from './CoursewareDashboardStudents.vue';
-import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
+import CoursewareCompanionOverlay from './layouts/CoursewareCompanionOverlay.vue';
 import { mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/block-mixin.js b/resources/vue/components/courseware/block-mixin.js
deleted file mode 100644
index 4e2dc77..0000000
--- a/resources/vue/components/courseware/block-mixin.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { mapActions, mapGetters } from 'vuex';
-
-export const blockMixin = {
-    computed: {
-        ...mapGetters({
-            getUserProgress: 'courseware-user-progresses/related',
-        }),
-        userProgress: {
-            get: function () {
-                return this.getUserProgress({ parent: this.block, relationship: 'user-progress' });
-            },
-            set: function (grade) {
-                this.userProgress.attributes.grade = grade;
-
-                return this.updateUserProgress(this.userProgress);
-            },
-        },
-    },
-    methods: {
-        ...mapActions({
-            updateUserProgress: 'courseware-user-progresses/update',
-        }),
-        getReadableDate(date) {
-            let locale = navigator.language ? navigator.language : 'de-DE';
-            return new Date(date).toLocaleDateString(locale, {
-                year: "numeric",
-                month: "2-digit",
-                day: "2-digit",
-            });
-        },
-        setShowEdit(state) {
-            this.showEdit = state;
-        },
-    },
-};
diff --git a/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue b/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
new file mode 100644
index 0000000..e346d20
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
@@ -0,0 +1,633 @@
+<template>
+    <div class="cw-block cw-block-audio">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
+                <audio
+                    :src="currentURL"
+                    class="cw-audio-player"
+                    ref="audio"
+                    @timeupdate="onTimeUpdateListener"
+                    @durationchange="setDuration"
+                    @ended="onEndedListener"
+                />
+                <div v-if="!emptyAudio" class="cw-audio-container">
+                    <div class="cw-audio-current-track">
+                        <p>{{ activeTrackName }}</p>
+                    </div>
+                    <div class="cw-audio-controls">
+                        <input
+                            class="cw-audio-range"
+                            ref="range"
+                            type="range"
+                            :value="currentSeconds"
+                            min="0"
+                            :max="Math.round(durationSeconds)"
+                            @input="rangeAction"
+                        />
+                        <span class="cw-audio-time">{{ currentTime }} {{ durationTime ? '/ ' + durationTime : '' }}</span>
+
+                        <button v-if="hasPlaylist" class="cw-audio-button cw-audio-prevbutton" :title="$gettext('Zurück')" @click="prevAudio" />
+                        <button v-if="!playing" class="cw-audio-button cw-audio-playbutton" :title="$gettext('Abspielen')" @click="playAudio" />
+                        <button v-if="playing" class="cw-audio-button cw-audio-pausebutton" :title="$gettext('Pause')" @click="pauseAudio" />
+                        <button v-if="hasPlaylist" class="cw-audio-button cw-audio-nextbutton" :title="$gettext('Weiter')" @click="nextAudio" />
+                        <button class="cw-audio-button cw-audio-stopbutton" :title="$gettext('Anhalten')" @click="stopAudio" />
+                    </div>
+                </div>
+                <div v-if="emptyAudio" class="cw-audio-empty">
+                    <p>{{ $gettext('Es ist keine Audio-Datei verfügbar') }}</p>
+                </div>
+                <div v-show="currentSource === 'studip_folder'" class="cw-audio-playlist-wrapper" :class="[!showRecorder && emptyAudio ? 'empty' : '']">
+                    <ul v-show="hasPlaylist" class="cw-audio-playlist" :class="[showRecorder ? 'with-recorder' : '']">
+                        <li v-for="(file, index) in files" :key="file.id">
+                            <a
+                                :aria-current="(index === currentPlaylistItem) ? 'true' : 'false'"
+                                :class="{
+                                    'is-playing': index === currentPlaylistItem && playing,
+                                    'current-item': index === currentPlaylistItem,
+                                }"
+                                :title="$gettext('Audiodatei:') + ' ' + file.name"
+                                href="#"
+                                class="cw-playlist-item"
+                                @click.prevent="setCurrentPlaylistItem(index)"
+                            >
+                                {{ file.name }}
+                            </a>
+                        </li>
+                    </ul>
+                    <div v-if="showRecorder && canGetMediaDevices" class="cw-audio-playlist-recorder">
+                        <button 
+                            v-show="!userRecorderEnabled"
+                            class="button"
+                            :disabled="!folderSelected || folderLoadError"
+                            :title="enableRecorderTitle"
+                            @click="enableRecorder"
+                        >
+                            {{ $gettext('Aufnahme aktivieren') }}
+                        </button>
+                        <button
+                            v-show="userRecorderEnabled && !isRecording && !newRecording"
+                            class="button"
+                            @click="startRecording"
+                        >
+                            {{ $gettext('Aufnahme starten') }}
+                        </button>
+                        <button
+                            v-show="newRecording && !isRecording"
+                            class="button"
+                            @click="startRecording"
+                        >
+                            {{ $gettext('Aufnahme wiederholen') }}
+                        </button>
+                        <button 
+                            v-show="isRecording"
+                            class="button"
+                            @click="stopRecording"
+                        >
+                            {{ $gettext('Aufnahme beenden') }}
+                        </button>
+                        <button 
+                            v-show="newRecording && !isRecording"
+                            class="button"
+                            @click="resetRecorder"
+                        >
+                            {{ $gettext('Aufnahme löschen') }}
+                        </button>
+                        <button 
+                            v-show="newRecording && !isRecording"
+                            class="button"
+                            @click="storeRecording"
+                        >
+                            {{ $gettext('Aufnahme speichern') }}
+                        </button>
+                        <span v-show="isRecording">
+                            {{ $gettext('Aufnahme läuft') }}: {{seconds2time(timer)}}
+                        </span>
+                    </div>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Überschrift') }}
+                        <input type="text" v-model="currentTitle" />
+                    </label>
+                    <label>
+                        {{ $gettext('Quelle') }}
+                        <select v-model="currentSource">
+                            <option value="studip_file">{{ $gettext('Dateibereich Datei') }}</option>
+                            <option value="studip_folder">{{ $gettext('Dateibereich Ordner') }}</option>
+                            <option value="web">{{ $gettext('Web-Adresse') }}</option>
+                        </select>
+                    </label>
+                    <label v-show="currentSource === 'web'">
+                        {{ $gettext('URL') }}
+                        <input type="text" v-model="currentWebUrl" />
+                    </label>
+                    <label v-show="currentSource === 'studip_file'">
+                        {{ $gettext('Datei') }}
+                        <courseware-file-chooser
+                            v-model="currentFileId"
+                            :isAudio="true"
+                            @selectFile="updateCurrentFile"
+                        />
+                    </label>
+                    <label v-show="currentSource === 'studip_folder'">
+                        {{ $gettext('Ordner') }}
+                        <courseware-folder-chooser v-model="currentFolderId" allowUserFolders />
+                    </label>
+                    <label v-show="currentSource === 'studip_folder'">
+                        {{ $gettext('Audio Aufnahmen zulassen') }}
+                        <span
+                            class="tooltip tooltip-icon"
+                            :data-tooltip="$gettext('Um Aufnahmen zu ermöglichen, muss ein Ordner ausgewählt werden.')"
+                        ></span>
+                        <select v-model="currentRecorderEnabled" :disabled="!folderSelected">
+                            <option :value="true">{{ $gettext('Ja') }}</option>
+                            <option :value="false">{{ $gettext('Nein') }}</option>
+                        </select>
+                    </label>
+                </form>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Audio-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-audio-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentSource: '',
+            currentFileId: '',
+            currentFolderId: '',
+            currentWebUrl: '',
+            currentFile: {},
+            currentSeconds: 0,
+            durationSeconds: 0,
+            playing: false,
+            currentPlaylistItem: 0,
+            currentRecorderEnabled: false,
+            userRecorderEnabled: false,
+            recorder: null,
+            chunks: [],
+            timer: 0,
+            isRecording: false,
+            newRecording: false,
+            folderLoadError: false
+        };
+    },
+    computed: {
+        ...mapGetters({
+            fileRefById: 'file-refs/byId',
+            relatedFileRefs: 'file-refs/related',
+            urlHelper: 'urlHelper',
+            userId: 'userId',
+            usersById: 'users/byId',
+            relatedTermOfUse: 'terms-of-use/related'
+        }),
+        files() {
+            const files =
+                this.relatedFileRefs({
+                    parent: { type: 'folders', id: this.currentFolderId },
+                    relationship: 'file-refs'
+                }) ?? [];
+
+            return files
+                .filter((file) => {
+                    if (this.relatedTermOfUse({parent: file, relationship: 'terms-of-use'}).attributes['download-condition'] !== 0) {
+                        return false;
+                    } 
+                    if (! file.attributes['mime-type'].includes('audio')) {
+                        return false;
+                    }
+
+                    return true;
+                })
+                .map(({ id, attributes }) => {
+                    return {
+                        id,
+                        name: attributes.name,
+                        download_url: this.urlHelper.getURL(
+                            'sendfile.php/',
+                            { type: 0, file_id: id, file_name: attributes.name },
+                            true
+                        ),
+                        mime_type: attributes['mime-type']
+                    };
+                });
+        },
+        currentTime() {
+            return this.seconds2time(this.currentSeconds);
+        },
+        durationTime() {
+            if (this.durationSeconds > 0) {
+                return this.seconds2time(this.durationSeconds);
+            }
+            return false;
+        },
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        source() {
+            return this.block?.attributes?.payload?.source;
+        },
+        fileId() {
+            return this.block?.attributes?.payload?.file_id;
+        },
+        folderId() {
+            return this.block?.attributes?.payload?.folder_id;
+        },
+        folderSelected() {
+            return this.currentFolderId !== '';
+        },
+        webUrl() {
+            return this.block?.attributes?.payload?.web_url;
+        },
+        recorderEnabled() {
+            return this.block?.attributes?.payload?.recorder_enabled;
+        },
+        showRecorder() {
+            return this.currentRecorderEnabled && this.currentSource === 'studip_folder';
+        },
+        hasPlaylist() {
+            return this.files.length > 0 && this.currentSource === 'studip_folder';
+        },
+        canGetMediaDevices() {
+            return navigator.mediaDevices !== undefined;
+        },
+        currentURL() {
+            if (this.currentSource === 'studip_file') {
+                return this.currentFile.download_url;
+            }
+            if (this.currentSource === 'studip_folder') {
+                if (this.files.length > 0) {
+                    return this.files[this.currentPlaylistItem].download_url;
+                } else {
+                    return '';
+                }
+            }
+            if (this.currentSource === 'web') {
+                return this.currentWebUrl;
+            }
+
+            return '';
+        },
+        activeTrackName() {
+            if (this.currentSource === 'studip_file') {
+                return this.currentFile.name;
+            }
+            if (this.currentSource === 'studip_folder') {
+                if (this.files.length > 0) {
+                    return this.files[this.currentPlaylistItem].name;
+                } else {
+                    return '';
+                }
+            }
+            if (this.currentSource === 'web') {
+                return this.currentWebUrl;
+            }
+
+            return '';
+        },
+        emptyAudio() {
+            if (this.currentSource === 'studip_folder' && this.currentFolderId !== '' && this.files.length > 0) {
+                return false;
+            }
+            if (this.currentSource === 'studip_file' && this.currentFileId !== '') {
+                return false;
+            }
+            if (this.currentSource === 'web' && this.currentWebUrl !== '') {
+                return false;
+            }
+            return true;
+        },
+        enableRecorderTitle() {
+            if (!this.folderSelected) {
+                return this.$gettext('Aufnahme nicht möglich, es wurde kein Ordner ausgewählt.');
+            }
+
+            if (this.folderLoadError) {
+                return this.$gettext('Aufnahme nicht möglich, der ausgewählte Ordner konnte nicht gefunden werden.');
+            }
+
+            return this.$gettext('Aktiviert die Aufnahmefunktion');
+        }
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            loadFileRef: 'file-refs/loadById',
+            loadRelatedFileRefs: 'file-refs/loadRelated',
+            updateBlock: 'updateBlockInContainer',
+            companionWarning: 'companionWarning',
+            companionSuccess: 'companionSuccess',
+            companionError: 'companionError',
+            createFile: 'createFile',
+        }),
+        initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentSource = this.source;
+            this.currentFileId = this.fileId;
+            this.currentWebUrl = this.webUrl;
+            if (this.currentFileId !== '') {
+                this.loadFile();
+            }
+            this.currentFolderId = this.folderId;
+            this.currentRecorderEnabled = this.recorderEnabled;
+        },
+        updateCurrentFile(file) {
+            this.currentFile = file;
+            this.currentFileId = file.id;
+        },
+        async getFolderFiles() {
+            try {
+                await this.loadRelatedFileRefs({
+                    parent: { type: 'folders', id: this.currentFolderId },
+                    relationship: 'file-refs',
+                    options: { include: 'terms-of-use' }
+                });
+            } catch(error) {
+                this.folderLoadError = true;
+            }
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.title = this.currentTitle;
+            attributes.payload.source = this.currentSource;
+            attributes.payload.file_id = '';
+            attributes.payload.web_url = '';
+            attributes.payload.folder_id = '';
+            attributes.payload.recorder_enabled = false;
+            if (this.currentSource === 'studip_file') {
+                if (this.currentFileId === '') {
+                    this.companionWarning({
+                        info: this.$gettext('Bitte wählen Sie eine Datei aus.')
+                    });
+                    return false;
+                }
+                attributes.payload.file_id = this.currentFileId;
+            } else if (this.currentSource === 'web') {
+                attributes.payload.web_url = this.currentWebUrl;
+            } else if (this.currentSource === 'studip_folder') {
+                if (this.currentFolderId === '') {
+                    this.companionWarning({
+                        info: this.$gettext('Bitte wählen Sie einen Ordner aus.')
+                    });
+                    return false;
+                }
+                attributes.payload.folder_id = this.currentFolderId;
+                attributes.payload.recorder_enabled = this.currentRecorderEnabled;
+            } else {
+                return false;
+            }
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+        rangeAction() {
+            if (this.$refs.range.value !== this.currentSeconds) {
+                this.$refs.audio.currentTime = this.$refs.range.value;
+            }
+        },
+        setDuration() {
+            let duration = this.$refs.audio.duration
+            if (!isNaN(duration) && isFinite(duration)) {
+                this.durationSeconds = duration;
+            } else {
+                this.durationSeconds = 0;
+            }
+        },
+        playAudio() {
+            const audio = this.$refs.audio;
+            let isSupported = 'unknown';
+            if (this.currentSource === 'studip_file') {
+                isSupported = audio.canPlayType(this.currentFile.mime_type);
+            }
+            if (this.currentSource === 'studip_folder') {
+                isSupported = audio.canPlayType(this.files[this.currentPlaylistItem].mime_type);
+            }
+            if (isSupported !== '') {
+                audio.play();
+                this.playing = true;
+            } else {
+                this.companionError({
+                    info: this.$gettext('Ihr Browser unterstützt dieses Audioformat leider nicht.')
+                });
+                if(this.hasPlaylist) {
+                    this.nextAudio();
+                }
+            }
+        },
+        pauseAudio() {
+            this.$refs.audio.pause();
+            this.playing = false;
+        },
+        stopAudio() {
+            this.pauseAudio();
+            this.$refs.audio.currentTime = 0;
+        },
+        onTimeUpdateListener() {
+            this.currentSeconds = this.$refs.audio.currentTime;
+        },
+        onEndedListener() {
+            this.stopAudio();
+            if(this.hasPlaylist) {
+                this.nextAudio();
+            }
+        },
+        seconds2time(seconds) {
+            seconds = Math.round(seconds);
+            let hours = Math.floor(seconds / 3600);
+            let minutes = Math.floor((seconds - hours * 3600) / 60);
+            let time = '';
+            seconds = seconds - hours * 3600 - minutes * 60;
+            if (hours !== 0) {
+                time = hours + ':';
+            }
+            if (minutes !== 0 || time !== '') {
+                minutes = minutes < 10 && time !== '' ? '0' + minutes : String(minutes);
+                time += minutes + ':';
+            }
+            if (time === '') {
+                time = seconds < 10 ? '0:0' + seconds : '0:' + seconds;
+            } else {
+                time += seconds < 10 ? '0' + seconds : String(seconds);
+            }
+            return time;
+        },
+        setCurrentPlaylistItem(index) {
+            if (this.currentPlaylistItem === index) {
+                if (this.playing) {
+                    this.pauseAudio();
+                } else {
+                    this.playAudio();
+                }
+            } else {
+                this.currentPlaylistItem = index;
+                this.$nextTick(()=> {
+                    this.playAudio();
+                });
+            }
+        },
+        prevAudio() {
+            this.stopAudio();
+            if (this.currentPlaylistItem !== 0) {
+                this.currentPlaylistItem = this.currentPlaylistItem - 1;
+            } else {
+                this.currentPlaylistItem = this.files.length - 1;
+            }
+            this.$nextTick(()=> {
+                this.playAudio();
+            });
+        },
+        nextAudio() {
+            this.stopAudio();
+            if (this.currentPlaylistItem < this.files.length - 1) {
+                this.currentPlaylistItem = this.currentPlaylistItem + 1;
+                this.$nextTick(()=> {
+                    this.playAudio();
+                });
+            }
+        },
+
+        async loadFile() {
+            const id = this.currentFileId;
+            await this.loadFileRef({ id });
+            const fileRef = this.fileRefById({ id });
+
+            if (fileRef) {
+                this.updateCurrentFile({
+                    id: fileRef.id,
+                    name: fileRef.attributes.name,
+                    download_url: this.urlHelper.getURL(
+                        'sendfile.php',
+                        { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
+                        true
+                    ),
+                    mime_type: fileRef.attributes['mime-type']
+                });
+            }
+        },
+        enableRecorder() {
+            if (!this.folderSelected || this.folderLoadError) {
+                return false;
+            }
+            let view = this;
+            navigator.mediaDevices.getUserMedia({ audio: true })
+                .then(function(stream) {
+                    view.recorder = new MediaRecorder(stream, {type: 'audio/webm; codecs:vp9' });
+                    view.userRecorderEnabled = true;
+                    view.recorder.ondataavailable = e => {
+                        view.chunks.push(e.data);
+                    };
+                })
+                .catch(() => {
+                    view.companionWarning({
+                        info: view.$gettext('Sie müssen ein Mikrofon freigeben, um eine Aufnahme starten zu können.')
+                    });
+                });
+        },
+        startRecording() {
+            let view = this;
+            this.chunks = [];
+            this.timer = 0;
+            this.recorder.start();
+            this.isRecording = true;
+            setTimeout(function(){ view.setTimer(); }, 1000);
+        },
+        stopRecording() {
+            this.isRecording = false;
+            this.newRecording = true;
+            this.recorder.stop();
+        },
+        setTimer() {
+            let view = this;
+            if (this.recorder.state === 'recording') {
+                this.timer++;
+                setTimeout(function(){ view.setTimer(); }, 1000);
+            }
+        },
+        async storeRecording() {
+            let view = this;
+            let user = this.usersById({id: this.userId});
+            let blob = new Blob(view.chunks, {type: 'audio/webm; codecs:vp9' });
+            let file = {
+                attributes: {
+                    name: (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '.webm'
+                },
+                relationships: {
+                    'terms-of-use': {
+                        data: {
+                            id: 'SELFMADE_NONPUB'
+                        }
+                    }
+                }
+            };
+            let fileObj = await this.createFile({
+                file: file,
+                filedata: blob,
+                folder: {id: this.currentFolderId}
+            });
+            if(fileObj && fileObj.type === 'file-refs') {
+                this.companionSuccess({
+                    info: this.$gettext('Die Aufnahme wurde erfolgreich im Dateibereich abgelegt.')
+                });
+            } else {
+                this.companionError({
+                    info: this.$gettext('Es ist ein Fehler aufgetreten! Die Aufnahme konnte nicht gespeichert werden.')
+                });
+            }
+            this.newRecording = false;
+            this.getFolderFiles();
+        },
+        resetRecorder() {
+            this.newRecording = false;
+            this.chunks = [];
+            this.timer = 0;
+            this.blob = null;
+        },
+    },
+    watch: {
+        currentFolderId(newState) {
+            if (newState === '') {
+                this.currentRecorderEnabled = false;
+            } else {
+                this.getFolderFiles();
+            }
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+    @import "../../../../assets/stylesheets/scss/courseware/blocks/audio.scss";
+</style>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
new file mode 100644
index 0000000..2c6ac69
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
@@ -0,0 +1,288 @@
+<template>
+    <div class="cw-block cw-block-before-after">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <TwentyTwenty v-if="!isEmpty" :before="currentBeforeUrl" :after="currentAfterUrl" />
+                <courseware-companion-box
+                    v-if="isEmpty && editMode"
+                    :msgCompanion="$gettext('Bitte wählen Sie ein Vorher- und ein Nachher-Bild aus.')"
+                    mood="pointing"
+                />
+            </template>
+            <template v-if="canEdit" #edit>
+                <courseware-tabs>
+                    <courseware-tab :index="0" :name="$gettext('Vorher')" :selected="true">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Quelle') }}
+                                <select v-model="currentBeforeSource">
+                                    <option value="studip">{{ $gettext('Dateibereich') }}</option>
+                                    <option value="web">{{ $gettext('Web-Adresse') }}</option>
+                                </select>
+                            </label>
+                            <label v-if="currentBeforeSource === 'web'">
+                                {{ $gettext('URL') }}
+                                <input type="text" v-model="currentBeforeWebUrl" />
+                            </label>
+                            <label v-if="currentBeforeSource === 'studip'">
+                                {{ $gettext('Bilddatei') }}
+                                <courseware-file-chooser
+                                    v-model="currentBeforeFileId"
+                                    :isImage="true"
+                                    @selectFile="updateCurrentBeforeFile"
+                                />
+                            </label>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab :index="1" :name="$gettext('Nachher')">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Quelle') }}
+                                <select v-model="currentAfterSource">
+                                    <option value="studip">{{ $gettext('Dateibereich') }}</option>
+                                    <option value="web">{{ $gettext('Web-Adresse') }}</option>
+                                </select>
+                            </label>
+                            <label v-if="currentAfterSource === 'web'">
+                                {{ $gettext('URL') }}
+                                <input type="text" v-model="currentAfterWebUrl" />
+                            </label>
+                            <label v-if="currentAfterSource === 'studip'">
+                                {{ $gettext('Bilddatei') }}
+                                <courseware-file-chooser
+                                    v-model="currentAfterFileId"
+                                    :isImage="true"
+                                    @selectFile="updateCurrentAfterFile"
+                                />
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Bildvergleich-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import TwentyTwenty from 'vue-twentytwenty';
+import 'vue-twentytwenty/dist/vue-twentytwenty.css';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-before-after-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {
+        TwentyTwenty,
+    }),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentBeforeSource: '',
+            currentBeforeFileId: '',
+            currentBeforeFile: {},
+            currentBeforeWebUrl: '',
+            currentAfterSource: '',
+            currentAfterFileId: '',
+            currentAfterFile: {},
+            currentAfterWebUrl: '',
+            afterFile: null,
+            beforeFile: null,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            viewMode: 'viewMode',
+        }),
+        beforeSource() {
+            return this.block?.attributes?.payload?.before_source;
+        },
+        beforeFileId() {
+            return this.block?.attributes?.payload?.before_file_id;
+        },
+        beforeWebUrl() {
+            return this.block?.attributes?.payload?.before_web_url;
+        },
+        afterSource() {
+            return this.block?.attributes?.payload?.after_source;
+        },
+        afterFileId() {
+            return this.block?.attributes?.payload?.after_file_id;
+        },
+        afterWebUrl() {
+            return this.block?.attributes?.payload?.after_web_url;
+        },
+        currentBeforeUrl() {
+            if (this.currentBeforeSource === 'studip' && this.currentBeforeFile?.meta) {
+                return this.currentBeforeFile.meta['download-url'];
+            } else if (this.currentBeforeSource === 'web') {
+                return this.currentBeforeWebUrl;
+            } else {
+                return '';
+            }
+        },
+        currentAfterUrl() {
+            if (this.currentAfterSource === 'studip' && this.currentAfterFile?.meta) {
+                return this.currentAfterFile.meta['download-url'];
+            } else if (this.currentAfterSource === 'web') {
+                return this.currentAfterWebUrl;
+            } else {
+                return '';
+            }
+        },
+        isEmpty() {
+            return this.currentBeforeUrl === '' || this.currentAfterUrl === '';
+        },
+        editMode() {
+            return this.viewMode === 'edit';
+        },
+    },
+    mounted() {
+        this.loadFileRefs(this.block.id).then((response) => {
+            for (let i = 0; i < response.length; i++) {
+                if (response[i].id === this.beforeFileId) {
+                    this.beforeFile = response[i];
+                }
+
+                if (response[i].id === this.afterFileId) {
+                    this.afterFile = response[i];
+                }
+            }
+
+            this.currentBeforeFile = this.beforeFile;
+            this.currentAfterFile = this.afterFile;
+        });
+
+        this.loadImages();
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            loadFileRefs: 'loadFileRefs',
+            companionWarning: 'companionWarning',
+        }),
+        loadImages() {
+            this.loadFileRefs(this.block.id).then((response) => {
+                for (let i = 0; i < response.length; i++) {
+                    if (response[i].id === this.beforeFileId) {
+                        this.beforeFile = response[i];
+                    }
+
+                    if (response[i].id === this.afterFileId) {
+                        this.afterFile = response[i];
+                    }
+                }
+
+                this.currentBeforeFile = this.beforeFile;
+                this.currentAfterFile = this.afterFile;
+            });
+        },
+
+        initCurrentData() {
+            this.currentBeforeSource = this.beforeSource;
+            this.currentBeforeFileId = this.beforeFileId;
+            this.currentBeforeWebUrl = this.beforeWebUrl;
+            this.currentAfterSource = this.afterSource;
+            this.currentAfterFileId = this.afterFileId;
+            this.currentAfterWebUrl = this.afterWebUrl;
+        },
+        updateCurrentBeforeFile(file) {
+            this.currentBeforeFile = file;
+            this.currentBeforeFileId = file.id;
+        },
+        updateCurrentAfterFile(file) {
+            this.currentAfterFile = file;
+            this.currentAfterFileId = file.id;
+        },
+        storeBlock() {
+            let cmpInfo = false;
+            let cmpInfoBefore = this.$gettext('Bitte wählen Sie ein Vorherbild aus.');
+            let cmpInfoAfter = this.$gettext('Bitte wählen Sie ein Nachherbild aus.');
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.before_source = this.currentBeforeSource;
+            attributes.payload.after_source = this.currentAfterSource;
+            if (this.currentAfterSource === 'studip') {
+                if (this.currentAfterFile === null) {
+                    cmpInfo = cmpInfoAfter;
+                } else {
+                    attributes.payload.after_file_id = this.currentAfterFile.id;
+                    attributes.payload.after_web_url = '';
+                }
+            } else if (this.currentAfterSource === 'web') {
+                if (this.currentAfterWebUrl === '') {
+                    cmpInfo = cmpInfoAfter;
+                } else {
+                    attributes.payload.after_file_id = '';
+                    attributes.payload.after_web_url = this.currentAfterWebUrl;
+                }
+            } else {
+                cmpInfo = cmpInfoAfter;
+            }
+            if (this.currentBeforeSource === 'studip') {
+                if (this.currentBeforeFile === null) {
+                    cmpInfo = cmpInfoBefore;
+                } else {
+                    attributes.payload.before_file_id = this.currentBeforeFile.id;
+                    attributes.payload.before_web_url = '';
+                }
+            } else if (this.currentBeforeSource === 'web') {
+                if (this.currentBeforeWebUrl === '') {
+                    cmpInfo = cmpInfoBefore;
+                } else {
+                    attributes.payload.before_file_id = '';
+                    attributes.payload.before_web_url = this.currentBeforeWebUrl;
+                }
+            } else {
+                cmpInfo = cmpInfoBefore;
+            }
+
+            if (cmpInfo) {
+                this.companionWarning({
+                    info: cmpInfo,
+                });
+                return false;
+            } else {
+                this.updateBlock({
+                    attributes: attributes,
+                    blockId: this.block.id,
+                    containerId: this.block.relationships.container.data.id,
+                });
+            }
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+.cw-block-before-after {
+    .twentytwenty-container {
+        width: 100% !important;
+        z-index: 19;
+        .twentytwenty-handle {
+            z-index: 18;
+        }
+        .twentytwenty-overlay {
+            z-index: 17;
+        }
+        img {
+            width: 100%;
+            z-index: 16;
+        }
+    }
+}
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue
new file mode 100644
index 0000000..07872d8
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue
@@ -0,0 +1,200 @@
+<template>
+    <div class="cw-block cw-block-biography cw-block-biography-achievements">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @storeEdit="storeBlock"
+            @showEdit="setShowEdit"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div class="cw-block-biography-content" >
+                    <div class="cw-block-biography-type" :class="'cw-block-biography-achievements-type-' + currentData.type">
+                        <h2>{{ typeName }}</h2>
+                    </div>
+                    <div class="cw-block-biography-details">
+                        <h3>
+                            {{ $gettext('Titel') }}: {{currentData.title}}
+                        </h3>
+                        <h4>
+                            <span v-show="currentData.type !== 'membership'">{{ $gettext('Datum') }}:</span>
+                            <span v-show="currentData.type === 'membership'">{{ $gettext('Startdatum') }}:</span>
+                            {{ getReadableDate(currentData.date) }}
+                        </h4>
+                        <h4 v-show="hasEndDate">
+                            {{ $gettext('Enddatum') }}: {{ getReadableDate(currentData.end_date)}}
+                        </h4>
+                        <h4 v-show="hasParticipation">
+                            {{ $gettext('Beteiligung') }}: {{ currentData.role }}
+                        </h4>
+                        <div>
+                            <h4>{{ $gettext('Beschreibung') }}:</h4>
+                            <div class="formatted-content ck-content" v-html="currentData.description"></div>
+                        </div>
+                    </div>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Type') }}
+                        <select v-model="currentData.type">
+                            <option value="certificate">{{ $gettext('Zertifikat') }}</option>
+                            <option value="accreditation">{{ $gettext('Akkreditierung') }}</option>
+                            <option value="award">{{ $gettext('Auszeichnung') }}</option>
+                            <option value="book">{{ $gettext('Buch') }}</option>
+                            <option value="publication">{{ $gettext('Veröffentlichung') }}</option>
+                            <option value="membership">{{ $gettext('Mitgliedschaft') }}</option>
+                        </select>
+                    </label>
+                    <label>
+                        {{ $gettext('Titel') }}
+                        <input type="text" v-model="currentData.title">
+                    </label>
+                    <label>
+                        <template v-if="!hasEndDate">{{ $gettext('Datum') }}</template>
+                        <template v-else>{{ $gettext('Startdatum') }}</template>
+                        <input type="date" v-model="currentData.date" />
+                    </label>
+                    <label v-show="hasEndDate">
+                        {{ $gettext('Enddatum') }}
+                        <input type="date" v-model="currentData.end_date" />
+                    </label>
+                    <label v-show="hasParticipation">
+                        {{ $gettext('Beteiligung') }}
+                        <input type="text" v-model="currentData.role">
+                    </label>
+                    <div class="label-text">
+                        {{ $gettext('Beschreibung') }}
+                    </div>
+                    <studip-wysiwyg v-model="currentData.description"></studip-wysiwyg>
+                </form>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Erfolge-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import StudipWysiwyg from '../../StudipWysiwyg.vue';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-biography-achievements-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, { StudipWysiwyg }),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentData: {},
+            showEdit: false,
+        }
+    },
+    computed: {
+        payload() {
+            return this.block?.attributes?.payload;
+        },
+        typeName() {
+            switch (this.currentData.type) {
+                case 'certificate':
+                    return this.$gettext('Zertifikat');
+                case 'accreditation':
+                    return this.$gettext('Akkreditierung');
+                case 'award':
+                    return this.$gettext('Auszeichnung');
+                case 'book':
+                    return this.$gettext('Buch');
+                case 'publication':
+                    return this.$gettext('Veröffentlichung');
+                case 'membership':
+                    return this.$gettext('Mitgliedschaft');
+                default:
+                    return '';
+            }
+        },
+        achievementClass() {
+            switch (this.currentData.type) {
+                case 'certificate':
+                case 'accreditation':
+                case 'award':
+                    return 'certificate';
+                case 'book':
+                case 'publication':
+                    return 'publication';
+                case 'membership':
+                    return 'membership';
+                default:
+                    return '';
+            }
+        },
+        hasParticipation() {
+            return ['book', 'publication'].includes(this.currentData.type);
+        },
+        hasEndDate() {
+            return this.currentData.type === 'membership';
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    updated() {
+        this.updateCanvas();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            if (this.payload) {
+                this.currentData = this.payload;
+                this.currentData.date = this.getInputDate(this.currentData.date);
+                this.currentData.end_date = this.getInputDate(this.currentData.end_date);
+            }
+        },
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
+        getInputDate(inputDate) {
+            let date = new Date(inputDate);
+            return date.getFullYear() + '-' +
+                ('0' + (date.getMonth() + 1)).slice(-2) + '-' +
+                ('0' + date.getDate()).slice(-2);
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {
+                type: this.currentData.type,
+                title: this.currentData.title,
+                date: new Date(this.currentData.date).getTime(),
+                end_date: new Date(this.currentData.end_date).getTime(),
+                role: this.currentData.role,
+                description: this.currentData.description
+            };
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+    watch: {
+        payload() {
+            if (!this.showEdit) {
+                this.initCurrentData();
+            }
+        },
+    }
+};
+</script>
+<style scoped lang="scss">
+    @import "../../../../assets/stylesheets/scss/courseware/blocks/biography.scss";
+</style>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue
new file mode 100644
index 0000000..c366618
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue
@@ -0,0 +1,258 @@
+<template>
+    <div class="cw-block cw-block-biography-career">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @closeEdit="initCurrentData"
+            @showEdit="setShowEdit"
+            @storeEdit="storeBlock"
+        >
+            <template #content>
+                <ol class="cw-timeline">
+                    <li 
+                        v-for="(item, index) in sortedItems"
+                        :key="index"
+                        class="cw-timeline-item"
+                    >
+                        <div class="cw-timeline-item-icon cw-timeline-item-icon-color-studip-blue">
+                            <studip-icon v-if="item.type === 'school'" shape="doctoral-cap" role="clickable" size="32"/>
+                            <studip-icon v-if="item.type === 'experience'" shape="tools" role="clickable" size="32"/>
+                        </div>
+                        <div
+                            class="cw-timeline-item-content cw-timeline-item-content-color-studip-blue"
+                            :class="[index % 2 === 0 ? 'left' : 'right',]"
+                        >
+                            <h3>{{ item.date ? getReadableDate(item.date) : ''}}{{ item.enddate ? ' - ' + getReadableDate(item.enddate) : '' }}</h3>
+                            <article>
+                                <header>{{ getItemTypeName(item.type) }}</header>
+                                <div v-if="item.type === 'school'">
+                                    <p>{{ $gettext('Bezeichnung der Qualifikation') }}: {{ item.qualification }}</p>
+                                    <p>{{ $gettext('Hauptfächer / Schwerpunkt') }}: {{ item.focus }}</p>
+                                    <p>{{ $gettext('berufliche Fähigkeiten') }}: {{ item.skills }}</p>
+                                </div>
+                                <div v-if="item.type === 'experience'">
+                                    <p>{{ $gettext('Name des Arbeitgebers') }}: {{ item.employer }}</p>
+                                    <p>{{ $gettext('Beruf / Funktion') }}: {{ item.job }}</p>
+                                </div>
+                            </article>
+                        </div>
+                    </li>
+                </ol>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Zeitliche Sortierung') }}
+                        <select v-model="currentSort">
+                            <option value="none">{{ $gettext('Keine') }}</option>
+                            <option value="asc">{{ $gettext('Aufsteigend') }}</option>
+                            <option value="desc">{{ $gettext('Absteigend') }}</option>
+                        </select>
+                    </label>
+                </form>
+                <button class="button add" @click="addItem">{{ $gettext('Ereignis hinzufügen') }}</button>
+                <courseware-tabs
+                    v-if="currentItems.length > 0"
+                    :setSelected="setItemTab"
+                    @selectTab="setItemTab = (parseInt($event.name.replace($gettext('Ereignis') +  ' ', '')) - 1)"
+                >
+                    <courseware-tab
+                        v-for="(item, index) in currentItems"
+                        :key="index"
+                        :index="index"
+                        :name="$gettext('Ereignis') +  ' ' + (index + 1).toString()"
+                        :selected="index === 0"
+                        canBeEmpty
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label class="col-1">
+                                {{ $gettext('Startdatum') }}
+                                <input type="date" v-model="item.date" required />
+                            </label>
+                            <label class="col-1">
+                                {{ $gettext('Enddatum') }}
+                                <input type="date" v-model="item.enddate" />
+                            </label>
+                            <label>
+                                {{ $gettext('Art') }}
+                                <select v-model="item.type">
+                                    <option value="school">{{ $gettext('Schul- und Berufsbildung') }}</option>
+                                    <option value="experience">{{ $gettext('Berufserfahrung') }}</option>
+                                </select>
+                            </label>
+                            <div v-show="item.type === 'school'">
+                                <label>
+                                    {{ $gettext('Bezeichnung der Qualifikation') }}
+                                    <input type="text" v-model="item.qualification" />
+                                </label>
+                                <label>
+                                    {{ $gettext('Hauptfächer / Schwerpunkt') }}
+                                    <input type="text" v-model="item.focus" />
+                                </label>
+                                <label>
+                                    {{ $gettext('berufliche Fähigkeiten') }}
+                                    <input type="text" v-model="item.skills" />
+                                </label>
+                            </div>
+                            <div v-show="item.type === 'experience'">
+                                <label>
+                                    {{ $gettext('Name des Arbeitgebers') }}
+                                    <input type="text" v-model="item.employer" />
+                                </label>
+                                <label>
+                                    {{ $gettext('Beruf / Funktion') }}
+                                    <input type="text" v-model="item.job" />
+                                </label>
+                            </div>
+                            <label>
+                                {{ $gettext('Beschreibung') }}
+                                <textarea v-model="item.description" />
+                            </label>
+                            <label v-if="currentItems.length > 1">
+                                <button class="button trash" @click="removeItem(index)">
+                                    {{ $gettext('Ereignis entfernen') }}
+                                </button>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Karriere-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-biography-career-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            showEdit: false,
+            setItemTab: 0,
+            currentItems: [],
+            currentSort: '',
+        }
+    },
+    computed: {
+        items() {
+            return this.block?.attributes?.payload?.items;
+        },
+        sort() {
+            return this.block?.attributes?.payload?.sort;
+        },
+        sortedItems() {
+            if (this.currentSort === 'none') {
+                return this.currentItems;
+            }
+            let view = this;
+            let items = _.cloneDeep(this.currentItems);
+            return items.sort((a, b) => {
+                let dateA = null;
+                let dateB = null;
+
+                if (a.time) {
+                    dateA = new Date(a.date + 'T' + a.time);
+                } else {
+                    dateA = new Date(a.date);
+                }
+                if (b.time) {
+                    dateB = new Date(b.date + 'T' + b.time);
+                } else {
+                    dateB = new Date(b.date);
+                }
+                if (view.currentSort === 'asc') {
+                    return dateA > dateB ? 1 : dateA < dateB ? -1 : 0;
+                }
+                if (view.currentSort === 'desc') {
+                    return dateA < dateB ? 1 : dateA > dateB ? -1 : 0;
+                }
+            });
+        }
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
+        initCurrentData() {
+            this.currentItems = this.items;
+            this.currentSort = this.sort;
+            this.setItemTab = 0;
+        },
+        addItem() {
+            this.currentItems.push({
+                title: '',
+                description: '',
+                date: '',
+                enddate: '',
+                type: 'school',
+                qualification: '',
+                focus: '',
+                skills: '',
+                employer: '',
+                job: '',
+            });
+            this.$nextTick(() => { this.setItemTab = this.currentItems.length - 1; });
+        },
+        removeItem(itemIndex){
+            this.currentItems = this.currentItems.filter((val, index) => {
+                return !(index === itemIndex);
+            });
+            this.$nextTick(() => { this.setItemTab = 0; });
+        },
+        getItemTypeName(type) {
+            switch (type) {
+                case 'school':
+                    return this.$gettext('Schul- und Berufsbildung');
+                case 'experience':
+                    return this.$gettext('Berufserfahrung');
+            }
+
+            return '';
+        },
+        storeBlock() {
+            let attributes = {
+                payload: {
+                    sort: this.currentSort,
+                    items: this.currentItems
+                }
+            };
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+    watch: {
+        items() {
+            if (!this.showEdit) {
+                this.initCurrentData();
+            }
+        },
+    }
+};
+</script>
+<style scoped lang="scss">
+    @import "../../../../assets/stylesheets/scss/courseware/blocks/timeline.scss";
+    @import "../../../../assets/stylesheets/scss/courseware/blocks/biography.scss";
+</style>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue
new file mode 100644
index 0000000..7a291bd
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue
@@ -0,0 +1,136 @@
+<template>
+    <div class="cw-block cw-block-biography cw-block-biography-goals">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @closeEdit="initCurrentData"
+            @showEdit="setShowEdit"
+            @storeEdit="storeBlock"
+        >
+            <template #content>
+                <div class="cw-block-biography-content" >
+                    <div class="cw-block-biography-type" :class="'cw-block-biography-goals-type-' + currentData.type">
+                        <h2>{{ goalTypeName }}</h2>
+                    </div>
+                    <div class="cw-block-biography-details formatted-content ck-content" v-html="currentData.description">
+                    </div>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default">
+                    <label for="type">
+                        {{ $gettext('Ziel') }}
+                        <select name="type" class="type" v-model="currentData.type">
+                            <option value="personal">{{ $gettext('Persönliches Ziel') }}</option>
+                            <option value="school">{{ $gettext('Schulisches Ziel') }}</option>
+                            <option value="academic">{{ $gettext('Akademisches Ziel') }}</option>
+                            <option value="professional">{{ $gettext('Berufliches Ziel') }}</option>
+                        </select>
+                    </label>
+                    <div class="label-text">
+                       {{ $gettext('Beschreibung') }}
+                    </div>
+                    <studip-wysiwyg v-model="currentData.description"></studip-wysiwyg>
+                </form>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Ziele-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import StudipWysiwyg from '../../StudipWysiwyg.vue';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-biography-goals-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, { StudipWysiwyg }),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            showEdit: false,
+            currentData: {
+                type: '',
+                description: ''
+            },
+        }
+    },
+    computed: {
+        type() {
+            return this.block?.attributes?.payload?.type;
+        },
+        description() {
+            return this.block?.attributes?.payload?.description;
+        },
+        goalTypeName() {
+            switch (this.currentData.type) {
+                case 'personal':
+                    return this.$gettext('Persönliches Ziel');
+                case 'school':
+                    return this.$gettext('Schulisches Ziel');
+                case 'academic':
+                    return this.$gettext('Akademisches Ziel');
+                case 'professional':
+                    return this.$gettext('Berufliches Ziel');
+            }
+
+            throw new Error('Undefined data type ' + this.currentData.type);
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            this.currentData = {
+                type: this.type,
+                description: this.description
+            };
+        },
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
+        storeBlock() {
+            let attributes = {
+                payload: {
+                    type: this.currentData.type,
+                    description: this.currentData.description
+                }
+            };
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+    watch: {
+        type() {
+            if (!this.showEdit) {
+                this.currentData.type = this.type;
+            }
+        },
+        description() {
+            if (!this.showEdit) {
+                this.currentData.description = this.description;
+            }
+        },
+    }
+};
+</script>
+<style scoped lang="scss">
+    @import "../../../../assets/stylesheets/scss/courseware/blocks/biography.scss";
+</style>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyPersonalInformationBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyPersonalInformationBlock.vue
new file mode 100644
index 0000000..8de1843
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBiographyPersonalInformationBlock.vue
@@ -0,0 +1,186 @@
+<template>
+    <div class="cw-block cw-block-biography cw-block-biography-personal-information">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @closeEdit="initCurrentData"
+            @showEdit="setShowEdit"
+            @storeEdit="storeBlock"
+        >
+            <template #content>
+                <div class="cw-block-biography-content" >
+                    <div class="cw-block-biography-type cw-block-biography-personal-information-type">
+                        <h2>{{currentData.name}}</h2>
+                    </div>
+                    <div class="cw-block-biography-details cw-block-biography-personal-information-details">
+                        <span>{{ $gettext('Geburtsort') }}:</span>
+                        <span>{{currentData.birthplace}}</span>
+
+                        <span>{{ $gettext('Geburtsdatum') }}:</span>
+                        <span>{{ getReadableDate(currentData.birthday) }}</span>
+
+                        <span>{{ $gettext('Geschlecht') }}:</span>
+                        <span>{{displayGenderText(currentData.gender)}}</span>
+
+                        <span>{{ $gettext('Familienstand') }}:</span>
+                        <span>{{ displayStatusText(currentData.status) }}</span>
+                    </div>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Name') }}
+                        <input type="text" v-model="currentData.name">
+                    </label>
+                    <label>
+                        {{ $gettext('Geburtsort') }}
+                        <input type="text" v-model="currentData.birthplace">
+                    </label>
+                    <label>
+                        {{ $gettext('Geburtsdatum') }}
+                        <input type="date" v-model="currentData.birthday" />
+                    </label>
+                    <label>
+                        {{ $gettext('Geschlecht') }}
+                        <select v-model="currentData.gender">
+                            <option value="none">{{ displayGenderText('none') }}</option>
+                            <option value="male">{{ displayGenderText('male') }}</option>
+                            <option value="female">{{ displayGenderText('female') }}</option>
+                            <option value="diverse">{{ displayGenderText('diverse') }}</option>
+                        </select>
+                    </label>
+                    <label>
+                        {{ $gettext('Familienstand') }}
+                        <select v-model="currentData.status">
+                            <option value="none">{{ displayStatusText('none') }}</option>
+                            <option value="single">{{ displayStatusText('single') }}</option>
+                            <option value="married">{{ displayStatusText('married') }}</option>
+                            <option value="widowed">{{ displayStatusText('widowed') }}</option>
+                            <option value="divorced">{{ displayStatusText('divorced') }}</option>
+                            <option value="registered-civil-partnership">{{ displayStatusText('registered-civil-partnership') }}</option>
+                            <option value="annulled-civil-partnership">{{ displayStatusText('annulled-civil-partnership') }}</option>
+                        </select>
+                    </label>
+                </form>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Persönlichen-Informationen-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-biography-personal-information-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            showEdit: false,
+            currentData: {},
+        }
+    },
+    computed: {
+        payload() {
+            return this.block?.attributes?.payload;
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        changeDate(date) {
+            this.currentData.birthday = date;
+        },
+        initCurrentData() {
+            if (this.payload) {
+                this.currentData = this.payload;
+                this.currentData.birthday = this.getInputDate(this.currentData.birthday);
+            }
+        },
+        getInputDate(inputDate) {
+            let date = new Date(inputDate);
+            return date.getFullYear() + '-' +
+                ('0' + (date.getMonth() + 1)).slice(-2) + '-' +
+                ('0' + date.getDate()).slice(-2);
+        },
+        displayGenderText(gender) {
+            switch (gender) {
+                case 'none':
+                    return this.$gettext('keine Angabe');
+                case 'male':
+                    return this.$gettext('männlich');
+                case 'female':
+                    return this.$gettext('Weiblich');
+                case 'diverse':
+                    return this.$gettext('divers');
+                default:
+                    return '';
+            }
+        },
+        displayStatusText(status) {
+            switch (status) {
+                case 'none':
+                    return this.$gettext('keine Angabe');
+                case 'single':
+                    return this.$gettext('ledig');
+                case 'married':
+                    return this.$gettext('verheiratet');
+                case 'widowed':
+                    return this.$gettext('verwitwet');
+                case 'divorced':
+                    return this.$gettext('geschieden');
+                case 'registered-civil-partnership':
+                    return this.$gettext('eingetragene Lebenspartnerschaft');
+                case 'annulled-civil-partnership':
+                    return this.$gettext('aufgehobene Lebenspartnerschaft');
+                default:
+                    return '';
+            }
+        },
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {
+                name: this.currentData.name,
+                birthplace: this.currentData.birthplace,
+                birthday: new Date(this.currentData.birthday).getTime(),
+                gender: this.currentData.gender,
+                status: this.currentData.status
+            };
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+    watch: {
+        payload() {
+            if (!this.showEdit) {
+                this.initCurrentData();
+            }
+        },
+    }
+};
+</script>
+<style scoped lang="scss">
+    @import "../../../../assets/stylesheets/scss/courseware/blocks/biography.scss";
+</style>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
new file mode 100644
index 0000000..3ffff18
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
@@ -0,0 +1,151 @@
+<template>
+    <div class="cw-block-actions">
+        <studip-action-menu
+            :items="menuItems"
+            :context="block.attributes.title"
+            @editBlock="editBlock"
+            @setVisibility="setVisibility"
+            @showInfo="showInfo"
+            @deleteBlock="deleteBlock"
+            @removeLock="removeLock"
+            @copyToClipboard="copyToClipboard"
+        />
+    </div>
+</template>
+
+<script>
+import StudipActionMenu from '../../StudipActionMenu.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-block-actions',
+    components: {
+        StudipActionMenu,
+    },
+    props: {
+        canEdit: Boolean,
+        deleteOnly: {
+            type: Boolean,
+            default: false
+        },
+        block: Object,
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            userIsTeacher: 'userIsTeacher',
+        }),
+        blocked() {
+            return this.block?.relationships?.['edit-blocker']?.data !== null;
+        },
+        blockerId() {
+            return this.blocked ? this.block?.relationships?.['edit-blocker']?.data?.id : null;
+        },
+        blockedByThisUser() {
+            return this.blocked && this.userId === this.blockerId;
+        },
+        blockedByAnotherUser() {
+            return this.blocked && this.userId !== this.blockerId;
+        },
+        menuItems() {
+            let menuItems = [];
+            if (this.canEdit) {
+                if (!this.deleteOnly) {
+                    if (!this.blocked) {
+                        menuItems.push({ id: 1, label: this.$gettext('Block bearbeiten'), icon: 'edit', emit: 'editBlock' });
+                        menuItems.push({
+                            id: 3,
+                            label: this.block.attributes.visible
+                                ? this.$gettext('unsichtbar setzen')
+                                : this.$gettext('sichtbar setzen'),
+                            icon: this.block.attributes.visible ? 'visibility-visible' : 'visibility-invisible', // do we change the icons ?
+                            emit: 'setVisibility',
+                        });
+                    }
+                    if (this.blocked && this.blockedByAnotherUser && this.userIsTeacher) {
+                        menuItems.push({
+                            id: 8,
+                            label: this.$gettext('Sperre aufheben'),
+                            icon: 'lock-unlocked',
+                            emit: 'removeLock',
+                        });
+                    }
+                    if (!this.blocked || this.blockedByThisUser) {
+                        menuItems.push({
+                            id: 9,
+                            label: this.$gettext('Block löschen'), 
+                            icon: 'trash',
+                            emit: 'deleteBlock' 
+                        });
+                    }
+                    menuItems.push({
+                        id: 2,
+                        label: this.$gettext('Block merken'),
+                        icon: 'clipboard',
+                        emit: 'copyToClipboard'
+                    });
+                    menuItems.push({
+                        id: 7,
+                        label: this.$gettext('Informationen zum Block'),
+                        icon: 'info',
+                        emit: 'showInfo',
+                    });
+                }
+            }
+            menuItems.sort((a, b) => {
+                return a.id > b.id ? 1 : b.id > a.id ? -1 : 0;
+            });
+
+            return menuItems;
+        }
+
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+        }),
+        menuAction(action) {
+            this[action]();
+        },
+        editBlock() {
+            this.$emit('editBlock');
+        },
+        showInfo() {
+            this.$emit('showInfo');
+        },
+        showExportOptions() {
+            this.$emit('showExportOptions');
+        },
+        async setVisibility() {
+            if (!this.blocked) {
+                await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
+            } else {
+                if (this.blockerId !== this.userId) {
+                    return false;
+                }
+            }
+            let attributes = {};
+            attributes.visible = !this.block.attributes.visible;
+
+            await this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+
+            await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' });
+        },
+        deleteBlock() {
+            this.$emit('deleteBlock');
+        },
+        removeLock() {
+            this.$emit('removeLock');
+        },
+        copyToClipboard() {
+            this.$emit('copyToClipboard');
+        }
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue b/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue
new file mode 100644
index 0000000..68dc648
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue
@@ -0,0 +1,138 @@
+<template>
+    <section class="cw-block-comments" :class="[emptyComments ? 'cw-block-comments-empty' : '']">
+        <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
+        <div class="cw-block-features-content">
+            <div class="cw-block-comments-items" v-show="!emptyComments" ref="commentsRef">
+                <courseware-talk-bubble
+                    v-for="comment in comments"
+                    :key="comment.id"
+                    :payload="buildPayload(comment)"
+                />
+            </div>
+            <div class="cw-block-comment-create">
+                <textarea v-model="createComment" :placeholder="placeHolder" spellcheck="true"></textarea>
+                <button class="button" @click="postComment"><translate>Senden</translate></button>
+            </div>
+        </div>
+    </section>
+</template>
+
+<script>
+import CoursewareTalkBubble from '../layouts/CoursewareTalkBubble.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-block-comments',
+    components: {
+        CoursewareTalkBubble,
+    },
+    props: {
+        block: Object,
+    },
+    data() {
+        return {
+            createComment: '',
+            placeHolder: this.$gettext('Stellen Sie eine Frage oder kommentieren Sie...'),
+            srMessage: ''
+        };
+    },
+    computed: {
+        ...mapGetters({
+            relatedUser: 'users/related',
+            userId: 'userId',
+            getComments: 'courseware-block-comments/related',
+        }),
+        comments() {
+            const parent = {
+                type: this.block.type,
+                id: this.block.id,
+            };
+
+            return this.getComments({ parent, relationship: 'comments' });
+        },
+        emptyComments() {
+            if (this.comments === null || this.comments.length === 0) {
+                return true;
+            }
+
+            return false;
+        }
+    },
+    methods: {
+        ...mapActions({
+            createComments: 'courseware-block-comments/create',
+            loadRelatedComments: 'courseware-block-comments/loadRelated'
+        }),
+        async loadComments() {
+            const parent = {
+                type: this.block.type,
+                id: this.block.id,
+            };
+            await this.loadRelatedComments({
+                parent,
+                relationship: 'comments',
+                options: {
+                    include: 'user',
+                },
+            });
+        },
+        async postComment() {
+            this.updateSrMessage(this.$gettext('Kommentar gesendet'));
+            const data = {
+                attributes: {
+                    comment: this.createComment
+                },
+                relationships: {
+                    block: {
+                        data: {
+                            id: this.block.id,
+                            type: this.block.type
+                        }
+                    }
+                }
+            };
+
+            await this.createComments(data);
+            this.loadComments();
+            this.createComment = '';
+        },
+        buildPayload(comment) {
+            const commenter = this.relatedUser({
+                parent: { id: comment.id, type: comment.type },
+                relationship: 'user',
+            });
+
+            const payload = {
+                id: comment.id,
+                own: comment.relationships.user.data.id === this.userId,
+                content: comment.attributes.comment,
+                chdate: comment.attributes.chdate,
+                mkdate: comment.attributes.mkdate,
+                user_id: commenter.id,
+                user_name: commenter.attributes['formatted-name'],
+                user_avatar: commenter.meta.avatar.small,
+            };
+
+            return payload;
+        },
+        updateSrMessage(message) {
+            this.srMessage = '';
+            this.srMessage = message;
+        }
+    },
+    mounted() {
+        this.loadComments();
+    },
+    updated() {
+        let ref = this.$refs["commentsRef"];
+        ref.scrollTop = ref.scrollHeight;
+    },
+    watch: {
+        comments() {
+            if (this.comments && this.comments.length > 0) {
+                this.$emit('hasComments');
+            }
+        }
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue b/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue
new file mode 100644
index 0000000..716e6ee
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue
@@ -0,0 +1,60 @@
+<template>
+    <div class="cw-block-discussion">
+        <courseware-collapsible-box
+            :title="text.comments"
+            :open="hasComments"
+        >
+            <courseware-block-comments
+            :block="block"
+            @hasComments="hasComments = true"
+            />
+        </courseware-collapsible-box>
+
+        <courseware-collapsible-box
+            v-if="canEdit || userIsTeacher"
+            :title="text.feedback"
+            :open="hasFeedback"
+        >
+            <courseware-block-feedback
+                :block="block"
+                :canEdit="canEdit"
+                @hasFeedback="hasFeedback = true"
+            />
+        </courseware-collapsible-box>
+    </div>
+</template>
+
+<script>
+import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue';
+import CoursewareBlockComments from './CoursewareBlockComments.vue';
+import CoursewareBlockFeedback from './CoursewareBlockFeedback.vue';
+import { mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-block-discussion',
+    components: {
+        CoursewareCollapsibleBox,
+        CoursewareBlockComments,
+        CoursewareBlockFeedback,
+    },
+    props: {
+        block: Object,
+        canEdit: Boolean
+    },
+    data() {
+        return {
+            hasComments: false,
+            hasFeedback: false,
+            text: {
+                comments: this.$gettext('Kommentare'),
+                feedback: this.$gettext('Feedback')
+            }
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userIsTeacher: 'userIsTeacher',
+        }),
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue b/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
new file mode 100644
index 0000000..d1ed09f
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
@@ -0,0 +1,50 @@
+<template>
+    <section class="cw-block-edit">
+        <header v-if="preview">{{ $gettext('Bearbeiten') }}</header>
+        <div class="cw-block-features-content">
+            <div @click="deactivateToolbar(); exitHandler = true;">
+                <slot name="edit" />
+            </div>
+            <div class="cw-button-box">
+                <button class="button accept" @click="$emit('store'); exitHandler = false;">{{ $gettext('Speichern') }}</button>
+                <button class="button cancel" @click="$emit('close'); exitHandler = false;">{{ $gettext('Abbrechen') }}</button>
+            </div>
+        </div>
+    </section>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-block-edit',
+    props: {
+        block: Object,
+        preview: Boolean
+    },
+    data() {
+        return {
+            originalBlock: Object,
+            exitHandler: false
+        };
+    },
+    beforeMount() {
+        this.originalBlock = this.block;
+    },
+    methods: {
+        ...mapActions({
+            coursewareBlockAdder: 'coursewareBlockAdder',
+            coursewareShowToolbar: 'coursewareShowToolbar'
+        }),
+        deactivateToolbar() {
+            this.coursewareBlockAdder({});
+            this.coursewareShowToolbar(false);
+        },
+    },
+    beforeDestroy() {
+        if (this.exitHandler) {
+            this.$emit('store');
+        }
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockExportOptions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockExportOptions.vue
new file mode 100644
index 0000000..71f6adc
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockExportOptions.vue
@@ -0,0 +1,18 @@
+<template>
+    <section class="cw-block-export-options">
+        <header><translate>Export Optionen</translate></header>
+        <div class="cw-block-features-content">
+            <button class="button" @click="$emit('close')"><translate>Schließen</translate></button>
+        </div>
+    </section>
+</template>
+
+<script>
+export default {
+    name: 'courseware-block-export-options',
+    components: {},
+    props: {
+        block: Object,
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockFeedback.vue b/resources/vue/components/courseware/blocks/CoursewareBlockFeedback.vue
new file mode 100644
index 0000000..dfc95dc
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockFeedback.vue
@@ -0,0 +1,141 @@
+<template>
+    <section
+        v-if="canEdit || userIsTeacher"
+        class="cw-block-feedback"
+        :class="[emptyFeedback ? 'cw-block-feedback-empty' : '']"
+    >
+        <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
+        <div class="cw-block-features-content">
+            <div class="cw-block-feedback-items" v-show="!emptyFeedback" ref="feedbacks">
+                <courseware-talk-bubble
+                    v-for="feedback in feedback"
+                    :key="feedback.id"
+                    :payload="buildPayload(feedback)"
+                />
+            </div>
+            <courseware-companion-box
+                v-if="!userIsTeacher && feedback.length === 0"
+                :msgCompanion="$gettext('Es wurde noch kein Feedback abgegeben.')"
+                mood="pointing"
+            />
+            <div v-if="userIsTeacher" class="cw-block-feedback-create">
+                <textarea v-model="feedbackText" :placeholder="placeHolder" spellcheck="true"></textarea>
+                <button class="button" @click="postFeedback">{{ $gettext('Senden') }}</button>
+            </div>
+        </div>
+    </section>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareTalkBubble from '../layouts/CoursewareTalkBubble.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+
+export default {
+    name: 'courseware-block-feedback',
+    components: {
+        CoursewareCompanionBox,
+        CoursewareTalkBubble,
+    },
+    props: {
+        block: Object,
+        canEdit: Boolean,
+    },
+    data() {
+        return {
+            feedbackText: '',
+            placeHolder: this.$gettext('Schreiben Sie ein Feedback...'),
+            srMessage: ''
+        };
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            getRelatedFeedback: 'courseware-block-feedback/related',
+            getRelatedUser: 'users/related',
+            userIsTeacher: 'userIsTeacher',
+        }),
+        feedback() {
+            const { id, type } = this.block;
+
+            return this.getRelatedFeedback({ parent: { id, type }, relationship: 'feedback' });
+        },
+        emptyFeedback() {
+            if (this.feedback === null || this.feedback.length === 0) {
+                return true;
+            }
+
+            return false;
+        }
+    },
+    methods: {
+        ...mapActions({
+            createFeedback: 'courseware-block-feedback/create',
+            loadRelatedFeedback: 'courseware-block-feedback/loadRelated',
+        }),
+        buildPayload(feedback) {
+            const { id, type } = feedback;
+            const user = this.getRelatedUser({ parent: { id, type }, relationship: 'user' });
+
+            return {
+                own: user.id === this.userId,
+                content: feedback.attributes.feedback,
+                chdate: feedback.attributes.chdate,
+                mkdate: feedback.attributes.mkdate,
+                user_name: user?.attributes?.['formatted-name'] ?? '',
+                user_avatar: user?.meta?.avatar.small,
+            };
+        },
+        async loadFeedback() {
+            const parent = {
+                type: this.block.type,
+                id: this.block.id,
+            };
+            await this.loadRelatedFeedback({
+                parent,
+                relationship: 'feedback',
+                options: {
+                    include: 'user',
+                },
+            });
+        },
+        async postFeedback() {
+            this.updateSrMessage(this.$gettext('Feedback gesendet'));
+            const data = {
+                attributes: {
+                    feedback: this.feedbackText,
+                },
+                relationships: {
+                    block: {
+                        data: {
+                            type: this.block.type,
+                            id: this.block.id,
+                        },
+                    },
+                },
+            };
+            await this.createFeedback(data, { root: true });
+            this.feedbackText = '';
+            this.loadFeedback();
+        },
+        updateSrMessage(message) {
+            this.srMessage = '';
+            this.srMessage = message;
+        }
+    },
+    async mounted() {
+        await this.loadFeedback(this.block.id);
+    },
+    updated() {
+        this.$refs.feedbacks.scrollTop = this.$refs.feedbacks.scrollHeight;
+    },
+    watch: {
+        feedback() {
+            if (this.feedback && this.feedback.length > 0) {
+                this.$emit('hasFeedback');
+            }
+        }
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockInfo.vue b/resources/vue/components/courseware/blocks/CoursewareBlockInfo.vue
new file mode 100644
index 0000000..705328e
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockInfo.vue
@@ -0,0 +1,65 @@
+<template>
+    <section class="cw-block-info">
+        <header>{{ $gettext('Informationen') }}</header>
+        <div class="cw-block-features-content cw-block-info-content">
+            <table class="cw-block-info-table">
+                <tr>
+                    <td>{{ $gettext('Blockbeschreibung') }}</td>
+                    <td><slot name="info" /></td>
+                </tr>
+                <tr>
+                    <td>{{ $gettext('Block wurde erstellt von') }}</td>
+                    <td>{{ owner }}</td>
+                </tr>
+                <tr>
+                    <td>{{ $gettext('Block wurde erstellt am') }}:</td>
+                    <td><iso-date :date="block.attributes.mkdate" /></td>
+                </tr>
+                <tr>
+                    <td>{{ $gettext('Zuletzt bearbeitet von') }}:</td>
+                    <td>{{ editor }}</td>
+                </tr>
+                <tr>
+                    <td>{{ $gettext('Zuletzt bearbeitet am') }}:</td>
+                    <td><iso-date :date="block.attributes.chdate" /></td>
+                </tr>
+            </table>
+            <button class="button" @click="$emit('close')">{{ $gettext('Schließen') }}</button>
+        </div>
+    </section>
+</template>
+
+<script>
+import IsoDate from '../layouts/IsoDate.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-block-info',
+    components: { IsoDate },
+    props: {
+        block: Object,
+    },
+    computed: {
+        ...mapGetters({
+            relatedUsers: 'users/related',
+        }),
+        owner() {
+            const owner = this.relatedUsers({
+                parent: this.block,
+                relationship: 'owner',
+            });
+
+            return owner?.attributes['formatted-name'] ?? '';
+        },
+
+        editor() {
+            const editor = this.relatedUsers({
+                parent: this.block,
+                relationship: 'editor',
+            });
+
+            return editor?.attributes['formatted-name'] ?? '';
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
new file mode 100644
index 0000000..794e4a9
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
@@ -0,0 +1,705 @@
+<template>
+    <div class="cw-block cw-block-canvas" ref="block">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentTitle" class="cw-block-title">
+                    {{ currentTitle }}
+                </div>
+                <div class="cw-canvasblock-toolbar">
+                    <div class="cw-canvasblock-buttonset">
+                        <button class="cw-canvasblock-reset" :title="$gettext('Zurücksetzen')" @click="reset"></button>
+                        <button class="cw-canvasblock-undo" :title="$gettext('Rückgängig')" @click="undo"></button>
+                        <button v-if="hasUploadFolder" class="cw-canvasblock-store" :title="$gettext('Bild im Dateibereich speichern')" @click="store"></button>
+                        <button v-if="canSwitchView" :class="viewButtonClass" :title="viewButtonText" @click="switchView"></button>
+                    </div>
+                    <div class="cw-canvasblock-buttonset">
+                        <button
+                            v-for="color in colors"
+                            :key="color.name"
+                            class="cw-canvasblock-color"
+                            :class="[currentColor === color.name ? 'selected-color' : '', color.name]"
+                            :title="color.title"
+                            @click="setColor(color.name)"
+                        />
+                    </div>
+                    <div class="cw-canvasblock-buttonset">
+                        <button
+                            class="cw-canvasblock-size cw-canvasblock-size-small"
+                            :class="{ 'selected-size': currentSize === 2 }"
+                            :title="$gettext('klein')"
+                            @click="setSize('small')"
+                        />
+                        <button
+                            class="cw-canvasblock-size cw-canvasblock-size-normal"
+                            :class="{ 'selected-size': currentSize === 5 }"
+                            :title="$gettext('normal')"
+                            @click="setSize('normal')"
+                        />
+                        <button
+                            class="cw-canvasblock-size cw-canvasblock-size-large"
+                            :class="{ 'selected-size': currentSize === 8 }"
+                            :title="$gettext('groß')"
+                            @click="setSize('large')"
+                        />
+                        <button
+                            class="cw-canvasblock-size cw-canvasblock-size-huge"
+                            :class="{ 'selected-size': currentSize === 12 }"
+                            :title="$gettext('riesig')"
+                            @click="setSize('huge')"
+                        />
+                    </div>
+                    <div class="cw-canvasblock-buttonset">
+                        <button
+                            class="cw-canvasblock-tool cw-canvasblock-tool-pen"
+                            :class="{ 'selected-tool': currentTool === 'pen' }"
+                            :title="$gettext('Zeichenwerkzeug')"
+                            @click="setTool('pen')"
+                        />
+                        <button
+                            class="cw-canvasblock-tool cw-canvasblock-tool-text"
+                            :class="{ 'selected-tool': currentTool === 'text' }"
+                            :title="$gettext('Textwerkzeug')"
+                            @click="setTool('text')"
+                        >
+                            T
+                        </button>
+                    </div>
+                </div>
+                <img :src="currentUrl" class="cw-canvasblock-original-img" ref="image" @load="buildCanvas" />
+                <input
+                    v-show="textInput"
+                    class="cw-canvasblock-text-input"
+                    ref="textInputField"
+                    @keyup="textInputKeyUp"
+                />
+                <canvas
+                    class="cw-canvasblock-canvas"
+                    :class="{
+                        'cw-canvasblock-tool-selected-pen': currentTool === 'pen',
+                        'cw-canvasblock-tool-selected-text': currentTool === 'text',
+                    }"
+                    ref="canvas"
+                    @mousedown="mouseDown"
+                    @mousemove="mouseMove"
+                    @mouseup="mouseUp"
+                    @mouseout="mouseUp"
+                    @mouseleave="mouseUp"
+
+                    @touchstart="touchStart"
+                    @touchmove="touchMove"
+                    @touchend="touchEnd"
+                />
+                <div class="cw-canvasblock-hints">
+                    <div v-show="write" class="messagebox messagebox_info cw-canvasblock-text-info">
+                        {{ $gettext('Texteingabe mit Enter-Taste bestätigen') }}
+                    </div>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <courseware-tabs>
+                    <courseware-tab
+                        :index="0"
+                        :name="$gettext('Grunddaten')"
+                        :selected="true"
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Überschrift') }}
+                                <input type="text" v-model="currentTitle" />
+                            </label>
+                            <label>
+                                {{ $gettext('Hintergrundbild') }}
+                                <select v-model="currentImage">
+                                    <option value="true">{{ $gettext('Ja') }}</option>
+                                    <option value="false">{{ $gettext('Nein') }}</option>
+                                </select>
+                            </label>
+                            <label v-if="currentImage === 'true'">
+                                {{ $gettext('Bilddatei') }}
+                                <courseware-file-chooser
+                                    v-model="currentFileId"
+                                    :isImage="true"
+                                    @selectFile="updateCurrentFile"
+                                />
+                            </label>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab
+                        :index="1"
+                        :name="$gettext('Einstellungen')"
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Speicherort') }}
+                                <courseware-folder-chooser v-model="currentUploadFolderId" :unchoose="true"/>
+                            </label>
+                            <label>
+                                {{ $gettext('Werte anderer Nutzer anzeigen') }}
+                                <select v-model="currentShowUserData">
+                                    <option value="off">{{ $gettext('deaktiviert') }}</option>
+                                    <option value="teacher">{{ $gettext('nur für Lehrende') }}</option>
+                                    <option value="all">{{ $gettext('für alle') }}</option>
+                                </select>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Leinwand-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-canvas-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentImage: '',
+            currentFileId: '',
+            currentUploadFolderId: '',
+            currentShowUserData: '',
+            currentUserView: 'own',
+            currentFile: {},
+
+            context: {},
+            paint: false,
+            write: false,
+            clickX: [],
+            clickY: [],
+            clickDrag: [],
+            clickColor: [],
+            colors: [
+                {rgba: 'rgba(255,255,255,1)', title: this.$gettext('weiß'), name: 'white'},
+                {rgba: 'rgba(52,152,219,1)', title: this.$gettext('blau'), name: 'blue'},
+                {rgba: 'rgba(46,204,113,1)', title: this.$gettext('grün'), name: 'green'},
+                {rgba: 'rgba(155,89,182,1)', title: this.$gettext('lila'), name: 'purple'},
+                {rgba: 'rgba(231,76,60,1)', title: this.$gettext('rot'), name: 'red'},
+                {rgba: 'rgba(254,211,48,1)', title: this.$gettext('gelb'), name: 'yellow'},
+                {rgba: 'rgba(243,156,18,1)', title: this.$gettext('orange'), name: 'orange'},
+                {rgba: 'rgba(149,165,166,1)', title: this.$gettext('grau'), name: 'grey'},
+                {rgba: 'rgba(52,73,94,1)', title: this.$gettext('dunkel grau'), name: 'darkgrey'},
+                {rgba: 'rgba(0,0,0,1)', title: this.$gettext('schwarz'), name: 'black'},
+            ],
+            currentColor: '',
+            currentColorRGBA: '',
+            sizes: { small: 2, normal: 5, large: 8, huge: 12 },
+            clickSize: [],
+            currentSize: '',
+            tools: { pen: 'pen', text: 'text' },
+            currentTool: '',
+            clickTool: [],
+            Text: [],
+            textInput: false,
+            file: null
+        };
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            getUserDataById: 'courseware-user-data-fields/byId',
+            usersById: 'users/byId',
+            relatedUserData: 'user-data-field/related'
+        }),
+        userData() {
+            return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id });
+        },
+        canvasDraw() {
+            if (this.userData !== undefined && this.userData.attributes.payload.canvas_draw) {
+                return this.userData.attributes.payload.canvas_draw;
+            } else {
+                return false;
+            }
+        },
+        allCanvasDraws() {
+            const parent = { type: 'courseware-blocks', id: this.block.id };
+            const relationship = 'user-data-field';
+            const userDataFields = this.relatedUserData({
+                parent: parent,
+                relationship: relationship,
+            });
+            let canvasDraws = [];
+            if (userDataFields?.length > 0) {
+                for (let userDataField of userDataFields) {
+                    // extracting the canvas draws of the other users.
+                    if (userDataField?.attributes?.payload?.canvas_draw &&
+                        userDataField?.relationships?.user?.data?.id !== this.userId ) {
+                        let canvas_draw = userDataField.attributes.payload.canvas_draw;
+                        let draw_obj = {
+                            clickX: JSON.parse(canvas_draw.clickX),
+                            clickY: JSON.parse(canvas_draw.clickY),
+                            clickDrag: JSON.parse(canvas_draw.clickDrag),
+                            clickColor: JSON.parse(canvas_draw.clickColor),
+                            clickSize: JSON.parse(canvas_draw.clickSize),
+                            clickTool: JSON.parse(canvas_draw.clickTool),
+                            Text: JSON.parse(canvas_draw.Text),
+                        };
+                        canvasDraws.push(draw_obj);
+                    }
+                }
+            }
+            return canvasDraws;
+        },
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        image() {
+            return this.block?.attributes?.payload?.image;
+        },
+        fileId() {
+            return this.block?.attributes?.payload?.file_id;
+        },
+        uploadFolderId() {
+            return this.block?.attributes?.payload?.upload_folder_id;
+        },
+        showUsersData() {
+            return this.block?.attributes?.payload?.show_usersdata;
+        },
+        currentUrl() {
+            if (this.currentFile?.meta) {
+                return this.currentFile.meta['download-url'];
+            } else if(this.currentFile?.download_url) {
+                    return this.currentFile.download_url;
+            } else {
+                return '';
+            }
+        },
+        currentFileName() {
+            if (this.currentFile?.attributes?.name) {
+                return this.currentFile.attributes.name;
+            } else {
+                return this.currentTitle + '.jpg';
+            }
+        },
+        hasUploadFolder() {
+            return this.currentUploadFolderId !== "";
+        },
+        canSwitchView() {
+            // this feature is not something to offer in the Arbeitsplatz!
+            let context = this.$store.getters.context;
+            if (context.type !== 'courses') {
+                return false;
+            }
+            if (this.currentShowUserData === 'off') {
+                return false;
+            }
+            if (this.currentShowUserData === 'teacher' && !this.isTeacher) {
+                return false;
+            }
+            return true;
+        },
+        viewButtonText() {
+            let text = this.$gettext('Werte anderer Nutzer anzeigen');
+            if (this.currentUserView == 'own') {
+                text = this.$gettext('Nur eigene Werte anzeigen');
+            }
+            return text;
+        },
+        viewButtonClass() {
+            return 'cw-canvasblock-show-' + this.currentUserView;
+        }
+    },
+    mounted() {
+        this.loadFileRefs(this.block.id).then((response) => {
+            this.file = response[0];
+            this.currentFile = this.file;
+            this.initCurrentData();
+            this.buildCanvas();
+        });
+        this.loadImageFile();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            loadFileRefs: 'loadFileRefs',
+            createFile: 'createFile',
+            companionSuccess: 'companionSuccess',
+            companionError: 'companionError',
+            updateUserDataFields: 'courseware-user-data-fields/update',
+            loadUserDataFields: 'loadUserDataFields',
+        }),
+        initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentImage = this.image;
+            this.currentFileId = this.fileId;
+            this.currentUploadFolderId = this.uploadFolderId;
+            this.currentShowUserData = this.showUsersData;
+            if (this.canvasDraw) {
+                this.clickX = JSON.parse(this.canvasDraw.clickX);
+                this.clickY = JSON.parse(this.canvasDraw.clickY);
+                this.clickDrag = JSON.parse(this.canvasDraw.clickDrag);
+                this.clickColor = JSON.parse(this.canvasDraw.clickColor);
+                this.clickSize = JSON.parse(this.canvasDraw.clickSize);
+                this.clickTool = JSON.parse(this.canvasDraw.clickTool);
+                this.Text = JSON.parse(this.canvasDraw.Text);
+            }
+        },
+        loadImageFile() {
+            this.loadFileRefs(this.block.id).then((response) => {
+                this.file = response[0];
+                this.currentFile = this.file;
+                this.initCurrentData();
+                this.buildCanvas();
+            });
+        },
+        updateCurrentFile(file) {
+            this.currentFile = file;
+            this.currentFileId = file.id;
+            this.buildCanvas();
+        },
+        setColor(color) {
+            if (this.write) {
+                return;
+            }
+            this.currentColor = color;
+            this.currentColorRGBA = this.colors.find(c => c.name === color).rgba;
+        },
+        setSize(size) {
+            if (this.textInput) {
+                return;
+            }
+            this.currentSize = this.sizes[size];
+        },
+        setTool(tool) {
+            if (this.write) {
+                this.clickX.pop();
+                this.clickY.pop();
+                this.clickDrag.pop();
+                this.clickColor.pop();
+                this.clickSize.pop();
+                this.clickTool.pop();
+                this.write = false;
+                this.textInput = false;
+            }
+            this.currentTool = this.tools[tool];
+        },
+        reset() {
+            this.clickX.length = 0;
+            this.clickY.length = 0;
+            this.clickDrag.length = 0;
+            this.clickColor.length = 0;
+            this.clickSize.length = 0;
+            this.clickTool.length = 0;
+            this.Text.length = 0;
+            this.paint = false;
+            this.write = false;
+            this.textInput = false;
+            this.redraw();
+        },
+        buildCanvas() {
+            let blockElem = this.$refs.block;
+            let image = this.$refs.image;
+            let canvas = this.$refs.canvas;
+            canvas.width = blockElem.offsetWidth - 2;
+            if (this.currentImage === 'true' && image.height > 0) {
+                canvas.height = Math.round((canvas.width / image.width) * image.height);
+            } else {
+                canvas.height = 500;
+            }
+            this.context = canvas.getContext('2d');
+            this.setColor('blue');
+            this.currentSize = this.sizes['normal'];
+            this.currentTool = this.tools['pen'];
+            this.redraw();
+        },
+        redraw() {
+            let view = this;
+            let context = view.context;
+            context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
+            context.fillStyle = '#ffffff';
+            context.fillRect(0, 0, context.canvas.width, context.canvas.height); // set background
+            if (view.currentImage === 'true') {
+                let outlineImage = new Image();
+                outlineImage.src = this.currentUrl;
+                context.drawImage(outlineImage, 0, 0, context.canvas.width, context.canvas.height);
+            }
+
+            context.lineJoin = 'round';
+            let ownCanvasDraw = {
+                clickX: view.clickX,
+                clickY: view.clickY,
+                clickDrag: view.clickDrag,
+                clickColor: view.clickColor,
+                clickSize: view.clickSize,
+                clickTool: view.clickTool,
+                Text: view.Text
+            }
+            let canvasDraws = [ownCanvasDraw];
+            if (this.currentUserView === 'all') {
+                canvasDraws = [ ...canvasDraws, ...view.allCanvasDraws ];
+            }
+
+            for (let draw of canvasDraws) {
+                for (var j = 0; j < draw.clickX.length; j++) {
+                    if (draw.clickTool[j] === 'pen') {
+                        context.beginPath();
+                        if (draw.clickDrag[j] && j) {
+                            context.moveTo(draw.clickX[j - 1], draw.clickY[j - 1]);
+                        } else {
+                            context.moveTo(draw.clickX[j] - 1, draw.clickY[j]);
+                        }
+                        context.lineTo(draw.clickX[j], draw.clickY[j]);
+                        context.closePath();
+                        context.strokeStyle = draw.clickColor[j];
+                        context.lineWidth = draw.clickSize[j];
+                        context.stroke();
+                    }
+                    if (draw.clickTool[j] === 'text') {
+                        let fontsize = draw.clickSize[j] * 6;
+                        context.font = fontsize + 'px Arial ';
+                        context.fillStyle = draw.clickColor[j];
+                        context.fillText(draw.Text[j], draw.clickX[j], draw.clickY[j] + fontsize);
+                    }
+                }
+            }
+        },
+        mouseDown(e) {
+            if (this.write) {
+                let view = this;
+                this.$refs.textInputField.focus();
+                window.setTimeout(function () {
+                    view.$refs.textInputField.focus();
+                }, 0);
+                return;
+            }
+            if (this.currentTool === 'pen') {
+                this.paint = true;
+                this.addClick(e.offsetX, e.offsetY, false);
+                this.redraw();
+            }
+            if (this.currentTool === 'text') {
+                this.write = true;
+                this.addClick(e.offsetX, e.offsetY, false);
+            }
+        },
+        mouseMove(e) {
+            if (this.paint) {
+                this.addClick(e.offsetX, e.offsetY, true);
+                this.redraw();
+            }
+        },
+        mouseUp(e) {
+            this.storeDraw();
+            this.paint = false;
+        },
+        touchStart(e) {
+            e.preventDefault();
+            if (this.write) {
+                return;
+            }
+            let canvas = this.$refs.canvas;
+            let mousePos = this.getTouchPos(canvas, e);
+            if(this.currentTool == 'pen') {
+                this.paint = true;
+                this.addClick(mousePos.x, mousePos.y, false);
+                this.redraw();
+            }
+            if(this.currentTool == 'text') {
+                this.write = true;
+                this.addClick(mousePos.x, mousePos.y, false);
+            }
+        },
+        touchMove(e) {
+            e.preventDefault();
+
+            let canvas = this.$refs.canvas;
+            let mousePos = this.getTouchPos(canvas, e);
+            if(this.paint){
+                this.addClick(mousePos.x, mousePos.y, true);
+                this.redraw();
+            }
+        },
+        touchEnd(e) {
+            this.storeDraw();
+            this.paint = false;
+        },
+        getTouchPos(canvasDom, touchEvent) {
+            var rect = canvasDom.getBoundingClientRect();
+            return {
+                x: touchEvent.touches[0].clientX - rect.left,
+                y: touchEvent.touches[0].clientY - rect.top
+            };
+        },
+        addClick(x, y, dragging) {
+            this.clickX.push(x);
+            this.clickY.push(y);
+            this.clickDrag.push(dragging);
+            this.clickColor.push(this.currentColorRGBA);
+            this.clickSize.push(this.currentSize);
+            this.clickTool.push(this.currentTool);
+            if (this.currentTool === 'text') {
+                this.enableTextInput(x, y);
+            } else {
+                this.Text.push('');
+            }
+        },
+        undo() {
+            let dragging = this.clickDrag[this.clickDrag.length - 1];
+            this.clickX.pop();
+            this.clickY.pop();
+            this.clickDrag.pop();
+            this.clickColor.pop();
+            this.clickSize.pop();
+            this.clickTool.pop();
+            if (this.write) {
+                this.textInput = false;
+                this.write = false;
+            } else {
+                this.Text.pop('');
+            }
+            if (dragging) {
+                this.undo();
+            }
+            this.redraw();
+        },
+        enableTextInput(x, y) {
+            let view = this;
+            let fontsize = this.currentSize * 6;
+            this.textInput = true;
+            let input = this.$refs.textInputField;
+            input.value = '';
+            input.style.position = 'absolute';
+            input.style.top = this.$refs.canvas.offsetTop + y + 'px';
+            input.style.left = 320 + x + 'px';
+            input.style.lineHeight = fontsize + 'px';
+            input.style.fontSize = fontsize + 'px';
+            input.style.width = '300px';
+            window.setTimeout(function () {
+                view.$refs.textInputField.focus();
+            }, 0);
+        },
+        textInputKeyUp(e) {
+            if (e.defaultPrevented) {
+                return;
+            }
+            let key = e.key || e.keyCode;
+            if (key === 'Enter' || key === 13) {
+                this.Text.push(this.$refs.textInputField.value);
+                this.textInput = false;
+                this.write = false;
+                this.redraw();
+            }
+            if (key === 'Escape' || key === 'Esc' || key === 27) {
+                this.clickX.pop();
+                this.clickY.pop();
+                this.clickDrag.pop();
+                this.clickColor.pop();
+                this.clickSize.pop();
+                this.clickTool.pop();
+                this.textInput = false;
+                this.write = false;
+            }
+        },
+        async storeDraw() {
+            let data = {};
+            data.type = 'courseware-user-data-fields';
+            data.id = this.block.relationships['user-data-field'].data.id;
+            data.relationships = {};
+            data.relationships.block = {};
+            data.relationships.block.data = {};
+            data.relationships.block.data.id = this.block.id;
+            data.relationships.block.data.type = this.block.type;
+            data.attributes = {};
+            data.attributes.payload = {};
+            data.attributes.payload.canvas_draw = {};
+            data.attributes.payload.canvas_draw.clickX = JSON.stringify(this.clickX);
+            data.attributes.payload.canvas_draw.clickY = JSON.stringify(this.clickY);
+            data.attributes.payload.canvas_draw.clickDrag = JSON.stringify(this.clickDrag);
+            data.attributes.payload.canvas_draw.clickColor = JSON.stringify(this.clickColor);
+            data.attributes.payload.canvas_draw.clickSize = JSON.stringify(this.clickSize);
+            data.attributes.payload.canvas_draw.clickTool = JSON.stringify(this.clickTool);
+            data.attributes.payload.canvas_draw.Text = JSON.stringify(this.Text);
+
+            await this.updateUserDataFields(data);
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.title = this.currentTitle;
+            attributes.payload.image = this.currentImage;
+            if (this.currentImage === 'true') {
+                attributes.payload.file_id = this.currentFileId;
+            } else {
+                attributes.payload.file_id = '';
+            }
+            attributes.payload.upload_folder_id = this.currentUploadFolderId;
+            attributes.payload.show_usersdata = this.currentShowUserData;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+        async store() {
+            let user = this.usersById({id: this.userId});
+            let imageBase64 = this.context.canvas.toDataURL("image/jpeg", 1.0);
+            let image = await fetch(imageBase64);
+            let imageBlob = await image.blob();
+            let file = {};
+            file.attributes = {};
+            if(this.currentImage === 'true') {
+                file.attributes.name = (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '_' + this.currentFile.attributes.name;
+            } else {
+                file.attributes.name = (user.attributes["formatted-name"]).replace(/\s+/g, '_') + '_' + this.block.attributes.title + '_' + this.block.id;
+            }
+
+            let img = await this.createFile({
+                file: file,
+                filedata: imageBlob,
+                folder: {id: this.currentUploadFolderId}
+            });
+
+            if(img && img.type === 'file-refs') {
+                this.companionSuccess({
+                    info: this.$gettext('Das Bild wurde erfolgreich im Dateibereich abgelegt.')
+                });
+            } else {
+                this.companionError({
+                    info: this.$gettext('Es ist ein Fehler aufgetretten! Das Bild konnte nicht gespeichert werden.')
+                });
+            }
+        },
+        async switchView() {
+            if (['own', 'all'].includes(this.currentUserView)) {
+                let newView = 'own';
+                if (this.currentUserView === 'own') {
+                    // we will get the latest draws by loading them each time the view is going to be switched to all!
+                    await this.loadUserDataFields(this.block.id).then(() => newView = 'all');
+                }
+                this.currentUserView = newView;
+            }
+        }
+    },
+    watch: {
+        currentUserView() {
+            this.redraw();
+        }
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/canvas.scss';
+</style>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue b/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue
new file mode 100644
index 0000000..42d2d81
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue
@@ -0,0 +1,325 @@
+<template>
+    <div class="cw-block cw-block-chart">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <canvas class="cw-chart-block-canvas" ref="chartCanvas" />
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label class="col-3">
+                        {{ $gettext('Beschriftung') }}
+                        <input type="text" v-model="currentLabel" @focusout="buildChart" />
+                    </label>
+                    <label class="col-3">
+                        {{ $gettext('Typ') }}
+                        <studip-select
+                            v-model="currentType"
+                            :options="chartTypes"
+                            :reduce="chartTypes => chartTypes.value"
+                            :clearable="false"
+                            @option:selected="buildChart"
+                        >
+                            <template #open-indicator="selectAttributes">
+                                <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
+                            </template>
+                            <template #selected-option="{name}">
+                                <span>{{name}}</span>
+                            </template>
+                            <template #option="{name}">
+                                <span>{{name}}</span>
+                            </template>
+                        </studip-select>
+                    </label>
+                </form>
+                <button class="button add" @click="addItem">{{ $gettext('Datensatz hinzufügen') }}</button>
+                <courseware-tabs
+                    v-if="currentContent.length > 0"
+                    :setSelected="setItemTab"
+                    @selectTab="setItemTab = (parseInt($event.name.replace($gettext('Datensatz') +  ' ', ''), 10) - 1)"
+                >
+                    <courseware-tab
+                        v-for="(item, index) in currentContent"
+                        :key="index"
+                        :index="index"
+                        :name="$gettext('Datensatz') +  ' ' + (index + 1).toString()"
+                        :selected="index === 0"
+                        canBeEmpty
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label class="col-1">
+                                {{ $gettext('Wert') }}
+                                <input type="number" v-model="item.value" @change="buildChart" />
+                            </label>
+                            <label class="col-2">
+                                {{ $gettext('Farbe') }}
+                                <studip-select
+                                    :options="colors"
+                                    :reduce="colors => colors.value"
+                                    label="rgb"
+                                    :clearable="false"
+                                    v-model="item.color"
+                                    @option:selected="buildChart"
+                                >
+                                    <template #open-indicator="selectAttributes">
+                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
+                                    </template>
+                                    <template #no-options>
+                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                                    </template>
+                                    <template #selected-option="{name, rgb}">
+                                        <span class="vs__option-color" :style="{'background-color': 'rgb(' + rgb + ')'}"></span><span>{{name}}</span>
+                                    </template>
+                                    <template #option="{name, rgb}">
+                                        <span class="vs__option-color" :style="{'background-color': 'rgb(' + rgb + ')'}"></span><span>{{name}}</span>
+                                    </template>
+                                </studip-select>
+                            </label>
+                            <label class="col-3">
+                                {{ $gettext('Bezeichnung') }}
+                                <input type="text" v-model="item.label" @focusout="buildChart" />
+                            </label>
+                            <button
+                                v-if="currentContent.length > 1"
+                                class="button trash"
+                                @click="removeItem(index)"
+                            >
+                                {{ $gettext('Datensatz entfernen') }}
+                            </button>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>                
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Diagramm-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import Chart from 'chart.js';
+import { mapActions } from 'vuex';
+
+
+export default {
+    name: 'courseware-chart-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            chart: null,
+            currentContent: [],
+            currentLabel: '',
+            currentType: '',
+            colors: [
+                { name:this.$gettext('Rot'), value: 'red', rgb: '192, 57, 43' },
+                { name:this.$gettext('Blau'), value: 'blue', rgb: '52, 152, 219' },
+                { name:this.$gettext('Gelb'), value: 'yellow', rgb: '241, 196, 15' },
+                { name:this.$gettext('Grün'), value: 'green', rgb: '46, 204, 113' },
+                { name:this.$gettext('Lila'), value: 'purple', rgb: '155, 89, 182' },
+                { name:this.$gettext('Orange'), value: 'orange', rgb: '230, 126, 34' },
+                { name:this.$gettext('Türkis'), value: 'turquoise', rgb: '26, 188, 156' },
+                { name:this.$gettext('Grau'), value: 'grey', rgb: '52, 73, 94' },
+                { name:this.$gettext('Hellgrau'), value: 'lightgrey', rgb: '149, 165, 166' },
+                { name:this.$gettext('Schwarz'), value: 'black', rgb: '0, 0, 0' },
+            ],
+            chartTypes: [
+                { name: this.$gettext('Säulendiagramm'), value: 'bar'},
+                { name: this.$gettext('Balkendiagramm'), value: 'horizontalBar'},
+                { name: this.$gettext('Kreisdiagramm'), value: 'pie'},
+                { name: this.$gettext('Ringdiagramm'), value: 'doughnut'},
+                { name: this.$gettext('Polardiagramm'), value: 'polarArea'},
+                { name: this.$gettext('Liniendiagramm'), value: 'line'},
+            ],
+            textRecordRemove: this.$gettext('Datensatz entfernen'),
+            setItemTab: 0,
+        };
+    },
+    computed: {
+        content() {
+            return this.block?.attributes?.payload?.content;
+        },
+        label() {
+            return this.block?.attributes?.payload?.label;
+        },
+        type() {
+            return this.block?.attributes?.payload?.type;
+        },
+        onlyRecord() {
+            return this.currentContent.length === 1;
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            this.currentContent = this.content;
+            this.currentLabel = this.label;
+            this.currentType = this.type;
+            this.setItemTab = 0;
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.content = this.currentContent;
+            attributes.payload.label = this.currentLabel;
+            attributes.payload.type = this.currentType;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+
+        addItem() {
+            this.currentContent.push({ value: '0', label: '', color: 'blue' });
+            this.$nextTick(() => { this.setItemTab = this.currentContent.length - 1; });
+        },
+
+        removeItem(recordIndex) {
+            this.currentContent = this.currentContent.filter((val, index) => {
+                return !(index === recordIndex);
+            });
+            this.buildChart();
+            this.$nextTick(() => { this.setItemTab = 0; });
+        },
+
+        buildChart() {
+            if (this.chart !== null) {
+                this.chart.destroy();
+            }
+            let ctx = this.$refs.chartCanvas.getContext('2d');
+            let type = this.currentType;
+            let label = this.currentLabel;
+            let labels = [];
+            let data = [];
+            let backgroundColor = [];
+            let borderColor = [];
+
+            this.currentContent.forEach((item) => {
+                labels.push(item.label);
+                data.push(item.value);
+                backgroundColor.push('rgba(' + this.colors.filter((color) => { return color.value === item.color })[0].rgb + ', 0.6)');
+                borderColor.push('rgba(' + this.colors.filter((color) => { return color.value === item.color })[0].rgb + ', 1.0)');
+            });
+
+            switch (type) {
+                case 'bar':
+                case 'horizontalBar':
+                    this.chart = new Chart(ctx, {
+                        type: type,
+                        data: {
+                            labels: labels,
+                            datasets: [
+                                {
+                                    label: label,
+                                    data: data,
+                                    backgroundColor: backgroundColor,
+                                    borderColor: borderColor,
+                                    borderWidth: 1,
+                                },
+                            ],
+                        },
+                        options: {
+                            scales: {
+                                yAxes: [
+                                    {
+                                        ticks: {
+                                            beginAtZero: true,
+                                        },
+                                    },
+                                ],
+                                xAxes: [
+                                    {
+                                        ticks: {
+                                            beginAtZero: true,
+                                        },
+                                    },
+                                ],
+                            },
+                            legend: {
+                                display: false,
+                            },
+                            title: {
+                                display: true,
+                                text: label,
+                            },
+                        },
+                    });
+                    break;
+                case 'pie':
+                case 'doughnut':
+                case 'polarArea':
+                    this.chart = new Chart(ctx, {
+                        type: type,
+                        data: {
+                            labels: labels,
+                            datasets: [
+                                {
+                                    data: data,
+                                    backgroundColor: backgroundColor,
+                                    borderWidth: 1,
+                                },
+                            ],
+                        },
+                        options: {
+                            title: {
+                                display: true,
+                                text: label,
+                            },
+                        },
+                    });
+                    break;
+                case 'line':
+                    this.chart = new Chart(ctx, {
+                        type: type,
+                        data: {
+                            labels: labels,
+                            datasets: [
+                                {
+                                    label: label,
+                                    data: data,
+                                    fill: false,
+                                    borderWidth: 2,
+                                    pointBackgroundColor: borderColor,
+                                },
+                            ],
+                        },
+                        options: {
+                            title: {
+                                display: true,
+                                text: label,
+                            },
+                        },
+                    });
+                    break;
+            }
+        },
+    },
+    watch: {
+        currentType() {
+            this.buildChart();
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareCodeBlock.vue b/resources/vue/components/courseware/blocks/CoursewareCodeBlock.vue
new file mode 100644
index 0000000..66f1069
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareCodeBlock.vue
@@ -0,0 +1,120 @@
+<template>
+    <div class="cw-block cw-block-code">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <pre v-show="currentContent !== ''" v-highlightjs="currentContent">
+                    <code ref="code" :class="[currentLang]"></code>
+                </pre>
+                <div v-show="currentLang !== ''" class="code-lang">
+                    <span>{{ currentLang }}</span>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Sprache') }}
+                        <input type="text" v-model="currentLang" />
+                    </label>
+                    <label>
+                        {{ $gettext('Quelltext') }}
+                        <textarea v-model="currentContent"></textarea>
+                    </label>
+                </form>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Quelltext-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import hljs from 'highlight.js';
+
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-code-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentLang: '',
+            currentContent: '',
+        };
+    },
+    computed: {
+        content() {
+            return this.block?.attributes?.payload?.content;
+        },
+        lang() {
+            return this.block?.attributes?.payload?.lang;
+        },
+    },
+    directives: {
+        highlightjs: {
+            deep: true,
+            bind(el, binding) {
+                let targets = el.querySelectorAll('code');
+                targets.forEach((target) => {
+                    if (binding.value) {
+                        target.innerHTML = binding.value;
+                    }
+                    hljs.highlightBlock(target);
+                });
+            },
+            componentUpdated(el, binding) {
+                let targets = el.querySelectorAll('code');
+                targets.forEach((target) => {
+                    if (binding.value) {
+                        target.innerHTML = binding.value;
+                        hljs.highlightBlock(target);
+                    }
+                });
+            },
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            this.currentLang = this.lang;
+            this.currentContent = this.content;
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.lang = this.currentLang;
+            attributes.payload.content = this.currentContent;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/code.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue b/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue
new file mode 100644
index 0000000..040d27f
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue
@@ -0,0 +1,119 @@
+<template>
+    <div class="cw-block cw-block-confirm">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            :defaultGrade="false"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div class="cw-block-title">
+                    {{ $gettext('Bestätigung') }}
+                </div>
+                <form class="default cw-block-confirm-content" prevent.default="">
+                    <label>
+                        <input type="checkbox" :disabled="confirm" :checked="confirm" @click="setConfirm" />
+                        <span>{{ currentText }}</span>
+                    </label>
+                </form>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Text') }}
+                        <input type="text" v-model="currentText" />
+                    </label>
+                </form>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Bestätigungs-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-confirm-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentText: '',
+            confirm: false,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            getUserDataById: 'courseware-user-data-fields/byId',
+        }),
+        text() {
+            return this.block?.attributes?.payload?.text;
+        },
+        userData() {
+            return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id });
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            updateUserDataFields: 'courseware-user-data-fields/update',
+        }),
+        initCurrentData() {
+            this.currentText = this.text;
+            if (this.userData?.attributes?.payload?.confirm) {
+                this.confirm = this.userData.attributes.payload.confirm;
+            }
+        },
+        async setConfirm() {
+            if (this.confirm) {
+                return;
+            }
+            let data = {};
+            data.type = 'courseware-user-data-fields';
+            data.id = this.block.relationships['user-data-field'].data.id;
+            data.attributes = {};
+            data.attributes.payload = {};
+            data.attributes.payload.confirm = true;
+            data.relationships = {};
+            data.relationships.block = {};
+            data.relationships.block.data = {};
+            data.relationships.block.data.id = this.block.id;
+            data.relationships.block.data.type = this.block.type;
+
+            await this.updateUserDataFields(data);
+            this.userProgress = 1;
+            this.confirm = true;
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.text = this.currentText;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/confirm.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareDateBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDateBlock.vue
new file mode 100644
index 0000000..7fbc1b2
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareDateBlock.vue
@@ -0,0 +1,205 @@
+<template>
+    <div class="cw-block cw-block-date">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentStyle === 'countdown'" class="cw-date-countdown">
+                    <div class="cw-date-countdown-digit" data-countdown="days">
+                        <div class="cw-date-countdown-number">{{ countdownDays }}</div>
+                        <div v-show="countdownDays === '01'" class="cw-date-countdown-label-sg">
+                            {{ $gettext('Tag') }}
+                        </div>
+                        <div v-show="countdownDays !== '01'" class="cw-date-countdown-label-pl">
+                            {{ $gettext('Tage') }}
+                        </div>
+                    </div>
+                    <div class="cw-date-countdown-digit" data-countdown="hours">
+                        <div class="cw-date-countdown-number">{{ countdownHours }}</div>
+                        <div v-show="countdownHours === '01'" class="cw-date-countdown-label-sg">
+                            {{ $gettext('Stunde') }}
+                        </div>
+                        <div v-show="countdownHours !== '01'" class="cw-date-countdown-label-pl">
+                            {{ $gettext('Stunden') }}
+                        </div>
+                    </div>
+                    <div class="cw-date-countdown-digit" data-countdown="minutes">
+                        <div class="cw-date-countdown-number">{{ countdownMinutes }}</div>
+                        <div v-show="countdownMinutes === '01'" class="cw-date-countdown-label-sg">
+                            {{ $gettext('Minute') }}
+                        </div>
+                        <div v-show="countdownMinutes !== '01'" class="cw-date-countdown-label-pl">
+                            {{ $gettext('Minuten') }}
+                        </div>
+                    </div>
+                    <div class="cw-date-countdown-digit" data-countdown="seconds">
+                        <div class="cw-date-countdown-number">{{ countdownSeconds }}</div>
+                        <div v-show="countdownSeconds === '01'" class="cw-date-countdown-label-sg">
+                            {{ $gettext('Sekunde') }}
+                        </div>
+                        <div v-show="countdownSeconds !== '01'" class="cw-date-countdown-label-pl">
+                            {{ $gettext('Sekunden') }}
+                        </div>
+                    </div>
+                </div>
+                <div v-if="currentStyle === 'date'" class="cw-date-date">
+                    <div class="cw-date-date-digits" data-date="date">
+                        <div class="cw-date-date-number">{{ currentDeDate }}</div>
+                    </div>
+                    <div class="cw-date-date-space"></div>
+                    <div class="cw-date-date-digits" data-date="time">
+                        <div class="cw-date-date-number">{{ currentTime }}</div>
+                    </div>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Datum') }}
+                        <input type="date" v-model="currentDate" @change="computeTimestamp" />
+                    </label>
+                    <label>
+                        {{ $gettext('Uhrzeit') }}
+                        <input type="time" v-model="currentTime" @change="computeTimestamp" />
+                    </label>
+                    <label>
+                        {{ $gettext('Layout') }}
+                        <select v-model="currentStyle">
+                            <option value="countdown">{{ $gettext('Countdown') }}</option>
+                            <option value="date">{{ $gettext('Datum') }}</option>
+                        </select>
+                    </label>
+                </form>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Termin-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions } from 'vuex';
+export default {
+    name: 'courseware-date-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentTimestamp: 0,
+            currentStyle: '',
+            currentTime: '',
+            currentDate: '',
+            currentDeDate: '',
+
+            countdownDays: '00',
+            countdownHours: '00',
+            countdownMinutes: '00',
+            countdownSeconds: '00',
+        };
+    },
+    computed: {
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        timestamp() {
+            return this.block?.attributes?.payload?.timestamp;
+        },
+        style() {
+            return this.block?.attributes?.payload?.style;
+        },
+        date() {
+            return new Date(this.currentTimestamp);
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+        if (this.currentStyle === 'countdown') {
+            this.countdown();
+        }
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            companionWarning: 'companionWarning',
+        }),
+        initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentTimestamp = this.timestamp;
+            this.currentDate =
+                this.date.getFullYear() +
+                '-' +
+                ('0' + (this.date.getMonth() + 1)).slice(-2) +
+                '-' +
+                ('0' + this.date.getDate()).slice(-2);
+            this.currentDeDate =
+                ('0' + this.date.getDate()).slice(-2) +
+                '.' +
+                ('0' + (this.date.getMonth() + 1)).slice(-2) +
+                '.' +
+                this.date.getFullYear();
+            this.currentTime = ('0' + this.date.getHours()).slice(-2) + ':' + ('0' + this.date.getMinutes()).slice(-2);
+            this.currentStyle = this.style;
+        },
+        countdown() {
+            let view = this;
+            setInterval(function () {
+                let now = new Date().getTime();
+                let distance = view.currentTimestamp - now;
+                if (distance < 0) {
+                    return;
+                }
+                view.countdownDays = Math.floor(distance / (1000 * 60 * 60 * 24));
+                view.countdownHours = ('0' + Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))).slice(
+                    -2
+                );
+                view.countdownMinutes = ('0' + Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60))).slice(-2);
+                view.countdownSeconds = ('0' + Math.floor((distance % (1000 * 60)) / 1000)).slice(-2);
+            }, 1000);
+        },
+        computeTimestamp() {
+            this.currentTimestamp = new Date(this.currentDate + ' ' + this.currentTime).getTime();
+        },
+        storeBlock() {
+            let cmpInfo = false;
+            if (this.currentDate === '') {
+                cmpInfo = this.$gettext('Bitte geben Sie ein Datum an.');
+            } else if (this.currentTime === '') {
+                cmpInfo = this.$gettext('Bitte geben Sie eine Uhrzeit an.');
+            }
+            if (cmpInfo) {
+                this.companionWarning({
+                    info: cmpInfo,
+                });
+                return false;
+            } else {
+                let attributes = {};
+                attributes.payload = {};
+                attributes.payload.timestamp = this.currentTimestamp;
+                attributes.payload.style = this.currentStyle;
+
+                this.updateBlock({
+                    attributes: attributes,
+                    blockId: this.block.id,
+                    containerId: this.block.relationships.container.data.id,
+                });
+            }
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/date.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
new file mode 100644
index 0000000..d11a538
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
@@ -0,0 +1,389 @@
+<template>
+    <div v-if="block.attributes.visible || canEdit" class="cw-default-block">
+        <div class="cw-content-wrapper" :class="[showEditMode ? 'cw-content-wrapper-active' : '']">
+            <header v-if="showEditMode" class="cw-block-header">
+                <a href="#" class="cw-block-header-toggle" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen">
+                    <studip-icon :shape="isOpen ? 'arr_1down' : 'arr_1right'" />
+                    <span>{{ blockTitle }}</span>
+                    <studip-icon v-if="blockedByAnotherUser" shape="lock-locked" />
+                    <span v-if="blockedByAnotherUser" class="cw-default-block-blocker-warning">
+                        {{ $gettextInterpolate($gettext('Wird im Moment von %{ userName } bearbeitet'), { userName: this.blockingUserName }) }}
+                    </span>
+                    <studip-icon v-if="!block.attributes.visible" shape="visibility-invisible" />
+                    <span v-if="!block.attributes.visible" class="cw-default-block-invisible-info">
+                        {{ $gettext('Unsichtbar für Nutzende ohne Schreibrecht') }}
+                    </span>
+                </a>
+                <courseware-block-actions
+                    :block="block"
+                    :canEdit="canEdit"
+                    :deleteOnly="deleteOnly"
+                    @editBlock="displayFeature('Edit')"
+                    @showInfo="displayFeature('Info')"
+                    @showExportOptions="displayFeature('ExportOptions')"
+                    @deleteBlock="displayDeleteDialog()"
+                    @removeLock="displayRemoveLockDialog()"
+                    @copyToClipboard="copyToClipboard"
+                />
+            </header>
+            <div v-show="isOpen">
+                <div v-if="showContent" class="cw-block-content">
+                    <slot name="content" />
+                </div>
+                <div v-if="showFeatures" class="cw-block-features cw-block-features-default">
+                    <courseware-block-export-options
+                        v-if="canEdit && showExportOptions"
+                        :block="block"
+                        @close="displayFeature(false)"
+                    />
+                    <courseware-block-edit
+                        v-if="canEdit && showEdit"
+                        :block="block"
+                        :preview="preview"
+                        @store="prepareStoreEdit"
+                        @close="closeEdit"
+                    >
+                        <template #edit>
+                            <slot name="edit" />
+                        </template>
+                    </courseware-block-edit>
+                    <courseware-block-info v-if="showInfo" :block="block" @close="displayFeature(false)">
+                        <template #info>
+                            <slot name="info" />
+                        </template>
+                    </courseware-block-info>
+                </div>
+            </div>
+        </div>
+        <div v-if="discussView" class="cw-discuss-wrapper">
+            <courseware-block-discussion
+                :block="block"
+                :canEdit="canEdit"
+            />
+        </div>
+        <studip-dialog
+            v-if="showDeleteDialog"
+            :title="textDeleteTitle"
+            :question="textDeleteAlert"
+            height="180"
+            width="360"
+            @confirm="executeDelete"
+            @close="closeDeleteDialog"
+        ></studip-dialog>
+        <studip-dialog
+            v-if="showRemoveLockDialog"
+            :title="textRemoveLockTitle"
+            :question="textRemoveLockAlert"
+            height="200"
+            width="450"
+            @confirm="executeRemoveLock"
+            @close="showRemoveLockDialog = false"
+        ></studip-dialog>
+
+    </div>
+</template>
+
+<script>
+import CoursewareBlockActions from './CoursewareBlockActions.vue';
+import CoursewareBlockDiscussion from './CoursewareBlockDiscussion.vue';
+import CoursewareBlockEdit from './CoursewareBlockEdit.vue';
+import CoursewareBlockExportOptions from './CoursewareBlockExportOptions.vue';
+import CoursewareBlockInfo from './CoursewareBlockInfo.vue';
+import StudipDialog from '../../StudipDialog.vue';
+import StudipIcon from '../../StudipIcon.vue';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+
+
+export default {
+    name: 'courseware-default-block',
+    mixins: [blockMixin],
+    components: {
+        CoursewareBlockActions,
+        CoursewareBlockDiscussion,
+        CoursewareBlockEdit,
+        CoursewareBlockExportOptions,
+        CoursewareBlockInfo,
+        StudipDialog,
+        StudipIcon,
+        
+    },
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        deleteOnly: {
+            type: Boolean,
+            default: false
+        },
+        isTeacher: Boolean,
+        preview: Boolean,
+        defaultGrade: {
+            type: Boolean,
+            default: true,
+        },
+    },
+    data() {
+        return {
+            showFeatures: false,
+            showExportOptions: false,
+            showEdit: false,
+            showInfo: false,
+            showContent: true,
+            showEditModeShortcut: false,
+            showDeleteDialog: false,
+            showRemoveLockDialog: false,
+            currentComments: [],
+            textDeleteTitle: this.$gettext('Block unwiderruflich löschen'),
+            textDeleteAlert: this.$gettext('Möchten Sie diesen Block wirklich löschen?'),
+            textRemoveLockTitle: this.$gettext('Sperre aufheben'),
+            textRemoveLockAlert: this.$gettext('Möchten Sie die Sperre dieses Blocks wirklich aufheben?'),
+            isOpen: true,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            blockTypes: 'blockTypes',
+            containerById: 'courseware-containers/byId',
+            context: 'context',
+            userId: 'userId',
+            userById: 'users/byId',
+            viewMode: 'viewMode',
+            currentElementisLink: 'currentElementisLink'
+        }),
+        showEditMode() {
+            let show = (this.viewMode === 'edit' || this.blockedByThisUser) && !this.currentElementisLink;
+            if (!show) {
+                this.displayFeature(false);
+            }
+            return show;
+        },
+        discussView() {
+            return this.viewMode === 'discuss';
+        },
+        blocked() {
+            return this.block?.relationships?.['edit-blocker']?.data !== null;
+        },
+        blockerId() {
+            return this.blocked ? this.block?.relationships?.['edit-blocker']?.data?.id : null;
+        },
+        blockedByThisUser() {
+            return this.blocked && this.userId === this.blockerId;
+        },
+        blockedByAnotherUser() {
+            return this.blocked && this.userId !== this.blockerId;
+        },
+        blockingUser() {
+            if (this.blockedByAnotherUser) {
+                return this.userById({id: this.blockerId});
+            }
+
+            return null;
+        },
+        blockingUserName() {
+            return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : '';
+        },
+        blockTitle() {
+            const type = this.block.attributes['block-type'];
+
+            return this.blockTypes.find((blockType) => blockType.type === type)?.title || this.$gettext('Fehler');
+        },
+        public() {
+            return this.context.type === 'public';
+        }
+    },
+    mounted() {
+        if (this.blocked) {
+            if (this.blockedByThisUser) {
+                this.displayFeature('Edit');
+            }
+        }
+        if (!this.public && this.userProgress && this.userProgress.attributes.grade === 0 && this.defaultGrade) {
+            this.userProgress = 1;
+        }
+    },
+    methods: {
+        ...mapActions({
+            companionInfo: 'companionInfo',
+            companionWarning: 'companionWarning',
+            companionSuccess: 'companionSuccess',
+            deleteBlock: 'deleteBlockInContainer',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+            loadContainer: 'loadContainer',
+            loadBlock: 'courseware-blocks/loadById',
+            updateContainer: 'updateContainer',
+            createClipboard: 'courseware-clipboards/create'
+        }),
+        async displayFeature(element) {
+            if (this.showEdit && element === 'Edit') {
+                return false;
+            }
+            this.showFeatures = false;
+            this.showExportOptions = false;
+            this.showEdit = false;
+            this.showInfo = false;
+            this.showContent = true;
+            if (element) {
+                if (element === 'Edit') {
+                    await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
+                    if (!this.blocked) {
+                        await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
+                        if (!this.preview) {
+                            this.showContent = false;
+                        }
+                        this['show' + element] = true;
+                        this.showFeatures = true;
+                    } else {
+                        if (this.blockedByThisUser) {
+                            if (!this.preview) {
+                                this.showContent = false;
+                            }
+                            this['show' + element] = true;
+                            this.showFeatures = true;
+                        } else {
+                            this.companionInfo({ info: this.$gettext('Dieser Block wird bereits bearbeitet.') });
+                        }
+                    }
+                } else {
+                    this['show' + element] = true;
+                    this.showFeatures = true;
+                }
+            }
+        },
+        prepareStoreEdit() {
+            // storeEdit is only emitted when the block is not in deleting process.
+            if (!this.showDeleteDialog) {
+                this.storeBlock();
+            }
+        },
+        async storeBlock() {
+            await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
+
+            if (this.blockedByThisUser) {
+                this.$emit('storeEdit');
+            }
+
+            if (this.blockedByAnotherUser) {
+                this.companionWarning({
+                    info: this.$gettextInterpolate(
+                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
+                        {blockingUserName: this.blockingUserName}
+                    )
+                });
+                this.displayFeature(false);
+                this.$emit('closeEdit');
+            }
+            if (this.blockerId === null) {
+                await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
+                this.$emit('storeEdit');
+            }
+        },
+        async closeEdit() {
+            await this.loadBlock({ id: this.block.id , options: { include: 'edit-blocker' } }); // has block editor lock changed?
+            this.displayFeature(false);
+            this.$emit('closeEdit');
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' });
+            }
+            this.loadBlock({ id: this.block.id , options: { include: 'edit-blocker' } }); // to update block editor lock
+        },
+        async displayDeleteDialog() {
+            await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
+            if (!this.blocked) {
+                await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
+                this.showDeleteDialog = true;
+            } else {
+                if (this.blockedByThisUser) {
+                    this.showDeleteDialog = true;
+                } else {
+                    this.companionInfo({
+                        info: this.$gettextInterpolate(
+                            this.$gettext('Löschen nicht möglich, da %{blockingUserName} den Block bearbeitet.'),
+                            {blockingUserName: this.blockingUserName}
+                        )
+                    });
+                }
+            }
+        },
+        async closeDeleteDialog() {
+            await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' });
+            }
+            this.showDeleteDialog = false;
+        },
+        async executeDelete() {
+            this.showDeleteDialog = false;
+            this.displayFeature(false);
+            this.$emit('closeEdit');
+            await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({
+                    info: this.$gettextInterpolate(
+                        this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'),
+                        {blockingUserName: this.blockingUserName}
+                    )
+                });
+                return false;
+            }
+            const containerId = this.block.relationships.container.data.id;
+            await this.loadContainer(containerId);
+            let container = this.containerById({id: containerId});
+            const structuralElementId = container.relationships['structural-element'].data.id;
+            let containerBlocks = container.relationships.blocks.data.map(block => {
+                return block.id;
+            });
+            let sections = container.attributes.payload.sections;
+
+            // lock parent container
+            await this.lockObject({ id: containerId, type: 'courseware-containers' });
+            // update container information
+            for (let i = 0; i < sections.length; i++) {
+                for (let j = 0; j < sections[i].blocks.length; j++) {
+                    let blockId = sections[i].blocks[j];
+                    if (!containerBlocks.includes(blockId) || blockId === this.block.id) {
+                        sections[i].blocks.splice(j, 1);
+                        j--;
+                    }
+                }
+            }
+            // update container
+            await this.updateContainer({ container, structuralElementId });
+            // unlock container
+            await this.unlockObject({ id: containerId, type: 'courseware-containers' });
+            await this.loadContainer(containerId);
+            this.deleteBlock({
+                blockId: this.block.id,
+                containerId: containerId,
+            });
+        },
+        displayRemoveLockDialog() {
+            this.showRemoveLockDialog = true;
+        },
+        async executeRemoveLock() {
+            await this.unlockObject({ id: this.block.id , type: 'courseware-blocks' });
+            await this.loadBlock({ id: this.block.id });
+            this.showRemoveLockDialog = false;
+        },
+        async copyToClipboard() {
+            const clipboard = {
+                attributes: {
+                    name: this.block.attributes.title,
+                    'block-id': this.block.id,
+                    'object-type': this.block.type,
+                    'object-kind': this.block.attributes['block-type'],
+                }
+            };
+
+            await this.createClipboard(clipboard, { root: true });
+            this.companionSuccess({ info: this.$gettext('Block wurde in Merkliste abgelegt.') });
+        }
+
+    },
+    watch: {
+        showEdit(state) {
+            this.$emit('showEdit', state);
+        }
+    }
+};
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue
new file mode 100644
index 0000000..5bfc2f7
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue
@@ -0,0 +1,291 @@
+<template>
+    <div class="cw-block cw-block-dialog-cards">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div class="cw-block-dialog-cards-content">
+                    <button
+                        class="cw-dialogcards-prev cw-dialogcards-navbutton"
+                        :class="{ 'cw-dialogcards-prev-disabled': hasNoPerv }"
+                        @click="prevCard"
+                        :title="hasNoPerv ? $gettext('keine vorherige Karte') : $gettext('zur vorherigen Karte')"
+                    ></button>
+                    <div class="cw-dialogcards">
+                        <div
+                            class="scene scene--card"
+                            :class="[card.active ? 'active' : '']"
+                            v-for="card in currentCards"
+                            :key="card.index"
+                        >
+                            <div
+                                class="card"
+                                tabindex="0"
+                                :title="$gettext('Karte umdrehen')"
+                                @click="flipCard"
+                                @keydown.enter="flipCard"
+                                @keydown.space="flipCard"
+                            >
+                                <div class="card__face card__face--front">
+                                    <img v-if="card.front_file.length !== 0" :src="card.front_file.download_url" />
+                                    <div v-else class="cw-dialogcards-front-no-image"></div>
+                                    <p>{{ card.front_text }}</p>
+                                </div>
+                                <div class="card__face card__face--back">
+                                    <img v-if="card.back_file.length !== 0" :src="card.back_file.download_url" />
+                                    <div v-else class="cw-dialogcards-back-no-image"></div>
+                                    <p>{{ card.back_text }}</p>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <button
+                        class="cw-dialogcards-next cw-dialogcards-navbutton"
+                        :class="{ 'cw-dialogcards-next-disabled': hasNoNext }"
+                        @click="nextCard"
+                        :title="hasNoNext ? $gettext('keine nächste Karte') : $gettext('zur nächsten Karte')"
+                    ></button>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <button class="button add" @click="addCard">{{ $gettext('Karte hinzufügen') }}</button>
+                <courseware-tabs
+                    v-if="currentCards.length > 0"
+                    :setSelected="setCardTab"
+                    @selectTab="activateCard(parseInt($event.name.replace($gettext('Karte') + ' ', '')) - 1)"
+                >
+                    <courseware-tab
+                        v-for="(card, index) in currentCards"
+                        :key="index"
+                        :index="index"
+                        :name="$gettext('Karte') + ' ' + (index + 1).toString()"
+                        :selected="index === 0"
+                        canBeEmpty
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Bild Vorderseite') }}:
+                                <courseware-file-chooser
+                                    v-model="card.front_file_id"
+                                    :isImage="true"
+                                    :canBeEmpty="true"
+                                    @selectFile="updateFile(index, 'front', $event)"
+                                />
+                            </label>
+                            <label>
+                                {{ $gettext('Text Vorderseite') }}:
+                                <input type="text" v-model="card.front_text" />
+                            </label>
+                            <label>
+                                {{ $gettext('Bild Rückseite') }}:
+                                <courseware-file-chooser
+                                    v-model="card.back_file_id"
+                                    :isImage="true"
+                                    :canBeEmpty="true"
+                                    @selectFile="updateFile(index, 'back', $event)"
+                                />
+                            </label>
+                            <label>
+                                {{ $gettext('Text Rückseite') }}:
+                                <input type="text" v-model="card.back_text" />
+                            </label>
+                            <label v-if="!onlyCard">
+                                <button class="button trash" @click="removeCard(index)">
+                                    {{ $gettext('Karte entfernen') }}
+                                </button>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Lernkarten-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-dialog-cards-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentCards: [],
+            setCardTab: 0,
+        };
+    },
+    computed: {
+        cards() {
+            return this.block?.attributes?.payload?.cards;
+        },
+        onlyCard() {
+            return this.currentCards.length === 1;
+        },
+        hasNoPerv() {
+            if (this.currentCards[0] !== undefined) {
+                return this.currentCards[0].active;
+            } else {
+                return true;
+            }
+        },
+        hasNoNext() {
+            if (this.currentCards[this.currentCards.length - 1] !== undefined) {
+                return this.currentCards[this.currentCards.length - 1].active;
+            } else {
+                return true;
+            }
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            if (this.cards !== '') {
+                let cards = JSON.parse(JSON.stringify(this.cards));
+                cards.forEach((card, index) => {
+                    card.active = false;
+                    if (index === 0) {
+                        card.active = true;
+                    }
+                });
+                this.currentCards = cards;
+            }
+            this.setCardTab = 0;
+        },
+        storeBlock() {
+            let cards = JSON.parse(JSON.stringify(this.currentCards));
+            // don't store the file object
+            cards.forEach((card) => {
+                delete card.front_file;
+                delete card.back_file;
+                delete card.active;
+            });
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.cards = cards;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+        updateFile(cardIndex, side, file) {
+            if (side === 'front') {
+                if (file) {
+                    this.currentCards[cardIndex].front_file_id = file.id;
+                    this.currentCards[cardIndex].front_file = file;
+                } else {
+                    this.currentCards[cardIndex].front_file_id = '';
+                    this.currentCards[cardIndex].front_file = [];
+                }
+            }
+            if (side === 'back') {
+                if (file) {
+                    this.currentCards[cardIndex].back_file_id = file.id;
+                    this.currentCards[cardIndex].back_file = file;
+                } else {
+                    this.currentCards[cardIndex].back_file_id = '';
+                    this.currentCards[cardIndex].back_file = [];
+                }
+            }
+        },
+        addCard() {
+            this.currentCards.push({
+                index: this.currentCards.length,
+                front_file_id: '',
+                front_file: [],
+                front_text: '',
+                back_file_id: '',
+                back_text: '',
+                back_file: [],
+                active: false,
+            });
+            const index = this.currentCards.length - 1;
+            this.activateCard(index);
+            this.$nextTick(() => {
+                this.setCardTab = index;
+            });
+        },
+        removeCard(cardIndex) {
+            this.currentCards = this.currentCards.filter((val, index) => {
+                return !(index === cardIndex);
+            });
+            this.$nextTick(() => {
+                this.setCardTab = 0;
+            });
+        },
+        flipCard(event) {
+            event.currentTarget.classList.toggle('is-flipped');
+        },
+        nextCard() {
+            let view = this;
+            this.currentCards.every((card, index) => {
+                if (card.active) {
+                    if (view.currentCards.length > index + 1) {
+                        card.active = false;
+                        view.currentCards[index + 1].active = true;
+                        view.setCardTab = index + 1;
+                    }
+                    return false; // end every
+                } else {
+                    return true; // continue every
+                }
+            });
+        },
+        prevCard() {
+            let view = this;
+            this.currentCards.every((card, index) => {
+                if (card.active) {
+                    if (index > 0) {
+                        card.active = false;
+                        view.currentCards[index - 1].active = true;
+                        view.setCardTab = index - 1;
+                    }
+                    return false; // end every
+                } else {
+                    return true; // continue every
+                }
+            });
+        },
+        activateCard(selectedIndex) {
+            selectedIndex = parseInt(selectedIndex);
+            if (selectedIndex > this.currentCards.length - 1) {
+                console.log('can not select this card');
+                return false;
+            }
+            this.currentCards.forEach((card, index) => {
+                if (index === selectedIndex) {
+                    card.active = true;
+                } else {
+                    card.active = false;
+                }
+            });
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/dialog-cards.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
new file mode 100644
index 0000000..7f7b10c
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
@@ -0,0 +1,809 @@
+<template>
+    <div class="cw-block cw-block-document">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="false"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div class="cw-pdf-main-container">
+                    <template v-if="hasFile">
+                        <div v-if="currentTitle !== ''" class="cw-block-title">
+                            {{ currentTitle }}
+                        </div>
+                        <div class="cw-pdf-toolbar">
+                            <div class="cw-pdf-toolbar-left">
+                                <div class="cw-pdf-toc">
+                                    <button
+                                        class="undecorated"
+                                        :class="{ active: pdfTOCDisplay }"
+                                        :title="$gettext('Inhaltsverzeichnis')"
+                                        :aria-pressed="pdfTOCDisplay ? 'true' : 'false'"
+                                        @click="toggleTOCViewer"
+                                    >
+                                        <studip-icon
+                                            shape="table-of-contents"
+                                            :role="
+                                                pdfTOC.length === 0
+                                                    ? 'inactive'
+                                                    : pdfTOCDisplay
+                                                    ? 'info_alt'
+                                                    : 'clickable'
+                                            "
+                                            :size="18"
+                                            class="text-bottom"
+                                        />
+                                    </button>
+                                </div>
+                                <div class="cw-pdf-search-toggle-btn">
+                                    <button
+                                        class="undecorated"
+                                        :class="{ active: showPdfSearchBox }"
+                                        :title="$gettext('Suche')"
+                                        :aria-pressed="showPdfSearchBox ? 'true' : 'false'"
+                                        @click="togglePdfSearchBox"
+                                    >
+                                        <studip-icon
+                                            shape="search"
+                                            :role="showPdfSearchBox ? 'info_alt' : 'clickable'"
+                                            :size="18"
+                                            class="text-bottom"
+                                        />
+                                    </button>
+                                </div>
+                                <div class="cw-pdf-search-box" v-show="showPdfSearchBox">
+                                    <input
+                                        ref="pdfSearchInput"
+                                        type="text"
+                                        v-model="pdfSearch"
+                                        @change="doSearchInPdf"
+                                    />
+                                    <div class="cw-pdf-search-navs" v-if="pdfSearchFoundNums > 1">
+                                        <button class="undecorated" @click="prevPdfSearch" :title="$gettext('Letzte')">
+                                            <studip-icon
+                                                shape="arr_1left"
+                                                :role="pdfSearchFoundSelectedIndex === 0 ? 'inactive' : 'clickable'"
+                                                :size="18"
+                                                class="text-bottom"
+                                            />
+                                        </button>
+                                        <button class="undecorated" @click="nextPdfSearch" :title="$gettext('Nächste')">
+                                            <studip-icon
+                                                shape="arr_1right"
+                                                :role="
+                                                    pdfSearchFoundSelectedIndex === pdfSearchFoundNums - 1
+                                                        ? 'inactive'
+                                                        : 'clickable'
+                                                "
+                                                :size="18"
+                                                class="text-bottom"
+                                            />
+                                        </button>
+                                    </div>
+                                    <span class="cw-pdf-search-num" v-if="pdfSearchFoundNums > 0">
+                                        {{ pdfSearchFoundSelectedIndex + 1 }} / {{ pdfSearchFoundNums }}
+                                        {{ $gettext('Treffer') }}
+                                    </span>
+                                </div>
+                                <div class="cw-pdf-page-nav">
+                                    <button
+                                        class="undecorated"
+                                        @click="prevPage"
+                                        :title="$gettext('Eine Seite zurück')"
+                                    >
+                                        <studip-icon
+                                            shape="arr_1up"
+                                            :role="pageNum - 1 === 0 ? 'inactive' : 'clickable'"
+                                            :size="18"
+                                            class="text-bottom"
+                                        />
+                                    </button>
+                                    <button class="undecorated" @click="nextPage" :title="$gettext('Eine Seite vor')">
+                                        <studip-icon
+                                            shape="arr_1down"
+                                            :role="pageNum === pageCount ? 'inactive' : 'clickable'"
+                                            :size="18"
+                                            class="text-bottom"
+                                        />
+                                    </button>
+                                    <input
+                                        type="text"
+                                        ref="pageNumInput"
+                                        class="cw-pdf-page-num"
+                                        :aria-label="$gettext('Seite')"
+                                        :value="pageNum"
+                                        @change="updatePageNum"
+                                    />
+                                    <span> {{ $gettext('von') }} {{ pageCount }} </span>
+                                </div>
+                            </div>
+                            <div class="cw-pdf-toolbar-middle">
+                                <div class="cw-pdf-zoom-buttons">
+                                    <button class="undecorated" @click="zoomIn" :title="$gettext('Vergrößern')">
+                                        <studip-icon shape="add" :size="18" class="text-bottom" />
+                                    </button>
+                                    <button class="undecorated" @click="zoomOut" :title="$gettext('Verkleinern')">
+                                        <studip-icon shape="remove" :size="18" class="text-bottom" />
+                                    </button>
+                                    <select v-model="currentScale" :aria-label="$gettext('Zoom')" @change="updateZoom">
+                                        <option v-show="false" :value="currentScale">{{ formattedZoom }}%</option>
+                                        <option v-for="(value, index) in scaleValues" :key="index" :value="value">
+                                            {{ value * 100 }}%
+                                        </option>
+                                    </select>
+                                </div>
+                                <div class="cw-pdf-rotate">
+                                    <button class="undecorated" @click="doRotatePdf" :title="$gettext('Drehen')">
+                                        <studip-icon shape="rotate-right" :size="18" class="text-bottom" />
+                                    </button>
+                                </div>
+                            </div>
+                            <div class="cw-pdf-toolbar-right">
+                                <div class="cw-pdf-handtool">
+                                    <button
+                                        class="undecorated"
+                                        :class="{ active: pdfHandTool }"
+                                        :title="$gettext('Hand-Werkzeug')"
+                                        :aria-pressed="pdfHandTool ? 'true' : 'false'"
+                                        @click="toggleHandTool"
+                                    >
+                                        <studip-icon
+                                            shape="hand"
+                                            :role="pdfHandTool ? 'info_alt' : 'clickable'"
+                                            :size="18"
+                                            class="text-bottom"
+                                        />
+                                    </button>
+                                </div>
+                                <div class="cw-pdf-download">
+                                    <a
+                                        v-if="downloadable === 'true'"
+                                        :href="currentUrl"
+                                        download
+                                        :title="$gettext('Speichern')"
+                                    >
+                                        <studip-icon shape="download" :size="18" class="text-bottom" />
+                                    </a>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="cw-pdf-outer-container" ref="outerContainer">
+                            <div class="cw-pdf-content">
+                                <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
+                                <div class="cw-pdf-sidebar" v-show="pdfTOCDisplay">
+                                    <ul class="cw-pdf-toc-list">
+                                        <CoursewarePDFTableOfContent
+                                            v-for="(item, index) in pdfTOC"
+                                            :item="item"
+                                            :key="index"
+                                            @tocPageNav="tocPageNav"
+                                        />
+                                    </ul>
+                                </div>
+                                <div
+                                    ref="container"
+                                    class="cw-pdf-viewer-container"
+                                    :class="{
+                                        'hand-cursor-grab': pdfHandTool,
+                                        grabbing: pdfGrabbing,
+                                        'has-error': pdfError,
+                                    }"
+                                    v-dragscroll="pdfHandTool"
+                                    @mousedown="handleMouseDown"
+                                    @mouseup="handleMouseUp"
+                                >
+                                    <div class="pdfViewer" />
+                                </div>
+                            </div>
+                            <div v-show="pdfError" class="cw-pdf-error-page">
+                                <courseware-companion-box
+                                    mood="sad"
+                                    :msgCompanion="$gettext('Es gab einen Fehler. Bitte versuchen Sie es erneut!')"
+                                >
+                                </courseware-companion-box>
+                            </div>
+                            <div ref="fakeContainer" class="cw-pdf-viewer-fake-container">
+                                <div class="pdfViewer" />
+                            </div>
+                        </div>
+                    </template>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Überschrift') }}
+                        <input type="text" v-model="currentTitle" />
+                    </label>
+                    <label>
+                        {{ $gettext('Datei') }}
+                        <courseware-file-chooser
+                            v-model="currentFileId"
+                            :isDocument="true"
+                            @selectFile="updateCurrentFile"
+                        />
+                    </label>
+                    <label>
+                        {{ $gettext('Download-Icon anzeigen') }}
+                        <select v-model="currentDownloadable">
+                            <option value="true">{{ $gettext('Ja') }}</option>
+                            <option value="false">{{ $gettext('Nein') }}</option>
+                        </select>
+                    </label>
+                    <label>
+                        {{ $gettext('Dateityp') }}
+                        <select v-model="currentDocType">
+                            <option value="pdf">PDF</option>
+                        </select>
+                    </label>
+                </form>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Dokument-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import CoursewarePDFTableOfContent from './CoursewarePDFTableOfContent.vue';
+import { getDocument } from 'pdfjs-dist';
+import {
+    DefaultAnnotationLayerFactory,
+    DefaultTextLayerFactory,
+    DefaultXfaLayerFactory,
+    DefaultStructTreeLayerFactory,
+    PDFFindController,
+    PDFLinkService,
+    PDFPageView,
+    PDFViewer,
+    EventBus,
+} from 'pdfjs-dist/web/pdf_viewer.js';
+// pdfjsWorker must be imported!
+import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
+import { dragscroll } from 'vue-dragscroll';
+
+import { mapActions } from 'vuex';
+import 'pdfjs-dist/web/pdf_viewer.css';
+
+export default {
+    name: 'courseware-document-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, { CoursewarePDFTableOfContent }),
+    directives: {
+        dragscroll,
+    },
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentFileId: '',
+            currentFile: {},
+            currentDownloadable: '',
+            currentDocType: '',
+
+            pdfError: false,
+            pdfBasePage: null,
+            pdfPage: null,
+            pdfTextContent: null,
+            pdfHandTool: false,
+            pdfGrabbing: false,
+            pdfTextLayer: null,
+            pdfAnnotationLayer: null,
+            pdfAnnotation: false,
+            pdfRotate: 0,
+            PdfViewer: null,
+            pdfEventBus: null,
+            pdfLinkService: null,
+            pdfFindController: null,
+            pdfDoc: null,
+            pdfLoadingTask: null,
+            pdfSearch: '',
+            pdfSearchMatchesMapping: [],
+            pdfSearchFoundNums: 0,
+            pdfSearchFoundSelectedIndex: 0,
+            pdfSearchHighlightedList: [],
+            showPdfSearchBox: false,
+            pdfTOC: [],
+            pdfTOCDisplay: false,
+            pageNum: 1,
+            pageCount: 0,
+            scale: 1,
+            currentScale: 1,
+            scaleValues: [0.5, 1, 1.5, 2, 3, 4],
+            file: null,
+
+            srMessage: '',
+        };
+    },
+    computed: {
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        fileDownloadable() {
+            return this.currentDownloadable === 'true';
+        },
+        downloadable() {
+            return this.block?.attributes?.payload?.downloadable ?? 'true';
+        },
+        fileId() {
+            return this.block?.attributes?.payload?.file_id;
+        },
+        docType() {
+            return this.block?.attributes?.payload?.doc_type;
+        },
+        currentUrl() {
+            if (this.currentFile?.meta) {
+                return this.currentFile.meta['download-url'];
+            } else {
+                return '';
+            }
+        },
+        hasFile() {
+            return this.currentFileId !== '';
+        },
+        formattedZoom() {
+            return Number.parseInt(this.scale * 100, 10);
+        },
+    },
+    watch: {
+        scale(newValue) {
+            let overflow = newValue > 1 ? 'auto' : 'hidden';
+            let container = this.$refs.container;
+            container.style.overflow = overflow;
+            this.currentScale = newValue;
+        },
+        pageNum(newValue) {
+            this.resetPdfViewer();
+        },
+        showPdfSearchBox() {
+            this.resetPdfSearch();
+        },
+    },
+    mounted() {
+        this.loadFileRefs(this.block.id).then((response) => {
+            this.file = response[0];
+            this.currentFile = this.file;
+            this.initPdfTask();
+        });
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            loadFileRefs: 'loadFileRefs',
+            companionWarning: 'companionWarning',
+        }),
+        initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentDownloadable = this.downloadable;
+            this.currentFileId = this.fileId;
+            this.currentDocType = this.docType;
+        },
+        updateCurrentFile(file) {
+            this.currentFile = file;
+            this.currentFileId = file.id;
+        },
+        initPdfTask() {
+            if (this.currentUrl) {
+                let view = this;
+                view.pdfEventBus = new EventBus();
+                view.pdfLoadingTask = getDocument(this.currentUrl).promise;
+                view.pdfLoadingTask.__PDFDocumentLoadingTask = true;
+                // Link Service
+                view.pdfLinkService = new PDFLinkService({
+                    eventBus: view.pdfEventBus,
+                });
+                // Find Controller
+                view.pdfFindController = new PDFFindController({
+                    eventBus: view.pdfEventBus,
+                    linkService: view.pdfLinkService,
+                });
+                // Annotation Layer
+                view.pdfAnnotationLayer = new DefaultAnnotationLayerFactory();
+                // Text Layer
+                view.pdfTextLayer = new DefaultTextLayerFactory();
+                // Load Pdf Document
+                view.loadPdfDocument();
+
+                // Handle search results.
+                view.pdfEventBus.on('updatetextlayermatches', ({ source, pageIndex }) => {
+                    if (view.pdfViewer.pdfPage._pageIndex == pageIndex) {
+                        setTimeout(() => {
+                            view.handleSearchMatches();
+                        }, 260);
+                    }
+                });
+            }
+        },
+        loadPdfDocument() {
+            if (this.pdfLoadingTask) {
+                let view = this;
+                view.pdfLoadingTask.then((pdfDocument) => {
+                    view.pdfDoc = pdfDocument;
+                    view.pageCount = pdfDocument.numPages;
+                    // get table of contents if any.
+                    view.loadPdfTOC();
+                    // Rendering PDF viewer
+                    view.loadPdfViewer();
+                    view.pdfLinkService.setDocument(view.pdfDoc, null);
+                    view.pdfFindController.setDocument(view.pdfDoc);
+                });
+            }
+        },
+        loadPdfTOC() {
+            if (this.pdfDoc) {
+                let view = this;
+                view.pdfTOC = [];
+                // Get the tree outline
+                view.pdfDoc.getOutline().then((outline) => {
+                    if (outline) {
+                        view.pdfTOC = outline;
+                    }
+                });
+            }
+        },
+        loadPdfViewer() {
+            if (this.pdfDoc) {
+                let view = this;
+                this.pdfError = false;
+                let container = this.$refs.container;
+                let outerContainer = this.$refs.outerContainer;
+                let fakeContainer = this.$refs.fakeContainer;
+                this.pdfDoc
+                    .getPage(parseInt(view.pageNum))
+                    .then((pdfPage) => {
+                        view.pdfPage = pdfPage;
+                        // Creating the page view with default parameters.
+                        let defaultViewport = pdfPage.getViewport({
+                            scale: 1.35,
+                        });
+
+                        view.pdfBasePage = new PDFViewer({
+                            container: fakeContainer,
+                            eventBus: view.pdfEventBus,
+                            findController: view.pdfFindController,
+                        });
+
+                        let pdfPageViewOptions = {
+                            container: container,
+                            id: view.pageNum,
+                            scale: view.scale,
+                            defaultViewport: defaultViewport,
+                            eventBus: view.pdfEventBus,
+                            findController: view.pdfFindController,
+                            textHighlighterFactory: view.pdfBasePage,
+                            xfaLayerFactory: view.pdfDoc.isPureXfa ? new DefaultXfaLayerFactory() : null,
+                            structTreeLayerFactory: new DefaultStructTreeLayerFactory(),
+                        };
+                        if (view.pdfHandTool === false) {
+                            pdfPageViewOptions.textLayerFactory = view.pdfTextLayer;
+                            pdfPageViewOptions.annotationLayerFactory = view.pdfAnnotationLayer;
+                        } else {
+                            pdfPageViewOptions.textLayerMode = 0;
+                            pdfPageViewOptions.annotationMode = 0;
+                        }
+                        // Force annotation to be disabled.
+                        if (!this.pdfAnnotation && pdfPageViewOptions?.annotationLayerFactory) {
+                            pdfPageViewOptions.annotationLayerFactory = null;
+                            pdfPageViewOptions.annotationMode = 0;
+                        }
+                        view.pdfViewer = new PDFPageView(pdfPageViewOptions);
+                        // Associates the actual page with the view, and drawing it
+                        view.pdfViewer.setPdfPage(view.pdfPage);
+                        // Set LinkService viewer
+                        view.pdfLinkService.setViewer(view.pdfViewer);
+                        // Set outer container height
+                        outerContainer.style.height = container.offsetHeight + 'px';
+                        view.renderPage();
+                    })
+                    .catch((err) => {
+                        console.log(err);
+                        outerContainer.style.minHeight = '350px';
+                        view.pdfError = true;
+                    });
+            }
+        },
+        renderPage() {
+            if (this.pdfViewer) {
+                this.updatePdfViewer();
+                this.pdfViewer.draw();
+                if (!this.pdfHandTool) {
+                    this.pdfViewer.textLayer.findController = this.pdfFindController;
+                }
+                if (this.pdfPage) {
+                    this.pdfPage.getTextContent().then((textContent) => {
+                        this.pdfTextContent = textContent;
+                    });
+                }
+                if (this.pdfSearchMatchesMapping.length) {
+                    this.pdfSearchDisplayHandler();
+                }
+            }
+        },
+        resetPdfViewer() {
+            this.pdfViewer.destroy();
+            let container = this.$refs.container;
+            while (!container.lastChild.classList.contains('pdfViewer')) {
+                container.removeChild(container.lastChild);
+            }
+            this.loadPdfViewer();
+        },
+        updatePdfViewer(resetScale = false) {
+            let updateArgs = {
+                scale: resetScale ? 1 : this.scale,
+                rotation: this.pdfRotate,
+            };
+            this.pdfViewer.update(updateArgs);
+        },
+        prevPage() {
+            if (this.pageNum <= 1) {
+                return;
+            }
+            this.pageNum--;
+        },
+        nextPage() {
+            if (this.pageNum >= this.pdfDoc.numPages) {
+                return;
+            }
+            this.pageNum++;
+        },
+        goToPage(page) {
+            const pageNum = Number.parseInt(page, 10);
+            if (pageNum < 1 || pageNum > this.pdfDoc.numPages) {
+                return;
+            }
+            this.pageNum = pageNum;
+        },
+        tocPageNav(dest) {
+            let view = this;
+            let destObj = dest.find(
+                (ref) =>
+                    typeof ref === 'object' &&
+                    ref !== null &&
+                    Number.isInteger(ref.num) &&
+                    ref.num >= 0 &&
+                    Number.isInteger(ref.gen) &&
+                    ref.gen >= 0
+            );
+            if (destObj) {
+                view.pdfDoc.getPageIndex(destObj).then((pageIndex) => {
+                    view.goToPage(pageIndex + 1);
+                });
+            }
+        },
+        updatePageNum() {
+            let pageNumInput = this.$refs.pageNumInput;
+            let value = Number.parseInt(pageNumInput.value, 10);
+            if (Number.isInteger(value) && value > 0 && value <= Number.parseInt(this.pageCount, 10)) {
+                this.pageNum = value;
+            } else {
+                pageNumInput.value = this.pageNum;
+            }
+        },
+        doRotatePdf() {
+            let rotationDegs = [0, 90, 180, 270, 360];
+            let index = rotationDegs.indexOf(this.pdfRotate);
+            let nextIndex = index + 1 >= rotationDegs.length ? 0 : index + 1;
+            let nextDeg = rotationDegs[nextIndex];
+            this.pdfRotate = nextDeg;
+            this.renderPage();
+            this.updateSrMessage(this.$gettext('gedreht'));
+        },
+        zoomIn() {
+            this.scale = this.scale < 4 ? (this.scale * 10 + 1) / 10 : this.scale;
+            this.renderPage();
+            this.updateSrMessage(this.$gettext('vergrößert'));
+        },
+        zoomOut() {
+            this.scale = this.scale > 0.1 ? (this.scale * 10 - 1) / 10 : this.scale;
+            this.renderPage();
+            this.updateSrMessage(this.$gettext('verkleinert'));
+        },
+        updateZoom(e) {
+            const value = e.target.value;
+            if (this.scale === value) {
+                return;
+            }
+            this.scale = value;
+            this.renderPage();
+            this.updateSrMessage(this.$gettext('Zoom Stufe ausgweählt'));
+        },
+        toggleHandTool() {
+            this.pdfHandTool = !this.pdfHandTool;
+            this.resetPdfViewer();
+            this.showPdfSearchBox = false;
+        },
+        handleHandToolDisplay(event) {
+            this.pdfGrabbing = event.type === 'mousedown';
+        },
+        handleMouseDown(e) {
+            this.handleHandToolDisplay(e);
+        },
+        handleMouseUp(e) {
+            this.handleHandToolDisplay(e);
+        },
+        togglePdfSearchBox() {
+            this.showPdfSearchBox = this.pdfHandTool ? false : !this.showPdfSearchBox;
+            if (this.showPdfSearchBox) {
+                this.$nextTick(() => {
+                    this.$refs.pdfSearchInput.focus();
+                });
+            }
+        },
+        handleSearchMatches() {
+            let view = this;
+            let allMatches = view.pdfFindController.pageMatches;
+            let totalMatches = 0;
+            let searchSelectIndex = 0;
+            let matchesPageCount = 0;
+            view.pdfSearchMatchesMapping = [];
+            for (let pageIndex = 0; pageIndex < view.pageCount; pageIndex++) {
+                let pageNum = pageIndex + 1;
+                let pageMatches = allMatches[pageIndex];
+                totalMatches += pageMatches.length;
+                if (pageMatches.length) {
+                    matchesPageCount++;
+                }
+                for (let i in pageMatches) {
+                    let matchIndex = parseInt(i, 10);
+                    let mappingObj = {
+                        selectIndex: searchSelectIndex,
+                        matchIndex: matchIndex,
+                        pageNum: pageNum,
+                    };
+                    view.pdfSearchMatchesMapping.push(mappingObj);
+                    searchSelectIndex++;
+                }
+            }
+            // Find next match if there the current page has nothing.
+            if (
+                view.pdfSearchFoundSelectedIndex === 0 &&
+                view.pdfViewer.pdfPage._pageIndex > 0 &&
+                matchesPageCount > 0
+            ) {
+                let nextMapped = view.pdfSearchMatchesMapping.filter(
+                    (map) => map.pageNum >= view.pdfViewer.pdfPage._pageIndex + 1
+                );
+                if (nextMapped.length) {
+                    view.pdfSearchFoundSelectedIndex = nextMapped[0].selectIndex;
+                }
+            }
+            view.pdfSearchFoundNums = totalMatches;
+            view.pdfSearchDisplayHandler();
+        },
+        doSearchInPdf() {
+            let findObj = {
+                type: '',
+                query: this.pdfSearch,
+                phraseSearch: true,
+                caseSensitive: false,
+                entireWord: true,
+                highlightAll: true,
+                findPrevious: false,
+                matchDiacritics: false,
+            };
+            this.pdfEventBus.dispatch('find', findObj);
+        },
+        prevPdfSearch() {
+            if (this.pdfSearchFoundSelectedIndex === 0) {
+                return;
+            }
+            this.pdfSearchFoundSelectedIndex--;
+            this.pdfSearchDisplayHandler();
+        },
+        nextPdfSearch() {
+            if (this.pdfSearchFoundSelectedIndex === this.pdfSearchFoundNums - 1) {
+                return;
+            }
+            this.pdfSearchFoundSelectedIndex++;
+            this.pdfSearchDisplayHandler();
+        },
+        pdfSearchDisplayHandler() {
+            // Go to page based on selected index.
+            let pageMatches = this.pdfSearchMatchesMapping.filter(
+                (map) => map.selectIndex === this.pdfSearchFoundSelectedIndex
+            );
+            if (pageMatches.length) {
+                let matchObj = pageMatches[0];
+                // A timeout of > 250ms is needed when page is changed!
+                let highlightRenderTimeout = 0;
+                if (matchObj.pageNum !== this.pageNum) {
+                    this.goToPage(matchObj.pageNum);
+                    highlightRenderTimeout = 260;
+                }
+                setTimeout(() => {
+                    this.setPdfSearchHighlighted();
+                    this.scrollToSearchFounds(matchObj.matchIndex);
+                }, highlightRenderTimeout);
+            }
+        },
+        scrollToSearchFounds(matchIndex) {
+            if (this.pdfSearchHighlightedList?.length) {
+                let selectedSpan = this.pdfSearchHighlightedList[matchIndex];
+                if (selectedSpan) {
+                    selectedSpan.classList.add('selected');
+                    selectedSpan.scrollIntoView({ behavior: 'smooth', block: 'center' });
+                }
+            }
+        },
+        setPdfSearchHighlighted() {
+            if (this.pdfViewer?.textLayer?.textDivs) {
+                let textDivs = this.pdfViewer.textLayer.textDivs;
+                let highlightedSpans = [];
+                for (let textSpan of textDivs) {
+                    if (textSpan?.children) {
+                        let children = [...textSpan.children];
+                        for (let child of children) {
+                            if (child.nodeName == 'SPAN' && child.classList.contains('highlight')) {
+                                child.classList.remove('selected');
+                                highlightedSpans.push(child);
+                            }
+                        }
+                    }
+                }
+                // Sort the array based on the top of the span.
+                highlightedSpans.sort((current, next) => {
+                    let currentTop = parseInt(current.parentNode.style.top, 10);
+                    let nextTop = parseInt(next.parentNode.style.top, 10);
+                    return currentTop > nextTop;
+                });
+                this.pdfSearchHighlightedList = highlightedSpans;
+            }
+        },
+        resetPdfSearch() {
+            this.pdfSearch = '';
+            this.pdfSearchFoundNums = 0;
+            this.pdfSearchFoundSelectedIndex = 0;
+            this.pdfSearchHighlightedList = [];
+            this.pdfSearchMatchesMapping = [];
+            this.doSearchInPdf();
+        },
+        toggleTOCViewer() {
+            if (this.pdfTOC.length) {
+                this.pdfTOCDisplay = !this.pdfTOCDisplay;
+            } else {
+                this.pdfTOCDisplay = false;
+            }
+        },
+        storeBlock() {
+            if (this.currentFile === undefined) {
+                this.companionWarning({
+                    info: this.$gettext('Bitte wählen Sie eine Datei aus.'),
+                });
+                return false;
+            } else {
+                let attributes = {};
+                attributes.payload = {};
+                attributes.payload.title = this.currentTitle;
+                attributes.payload.file_id = this.currentFile.id;
+                attributes.payload.downloadable = this.currentDownloadable.toString();
+                attributes.payload.doc_type = this.currentDocType;
+
+                this.updateBlock({
+                    attributes: attributes,
+                    blockId: this.block.id,
+                    containerId: this.block.relationships.container.data.id,
+                });
+            }
+        },
+        updateSrMessage(message) {
+            this.srMessage = '';
+            this.srMessage = message;
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/document.scss';
+</style>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue
new file mode 100644
index 0000000..2897b08
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue
@@ -0,0 +1,286 @@
+<template>
+    <div class="cw-block cw-block-download">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            :defaultGrade="false"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
+                <div v-if="currentFile !== null" class="cw-block-download-content">
+                    <div v-if="currentInfo !== '' && !userHasDownloaded" class="messagebox messagebox_info">
+                        {{ currentInfo }}
+                    </div>
+                    <div v-if="currentSuccess !== '' && userHasDownloaded" class="messagebox messagebox_info">
+                        {{ currentSuccess }}
+                    </div>
+                    <div class="cw-block-download-file-item">
+                        <a
+                            target="_blank"
+                            :download="currentFile.name"
+                            :title="$gettext('Datei herunterladen')"
+                            :href="currentFile.download_url"
+                            @click="handleDownload"
+                        >
+                            <span class="cw-block-file-info" :class="['cw-block-file-icon-' + currentFile.icon]">
+                                {{ currentFile.name }}
+                            </span>
+                        </a>
+                    </div>
+                </div>
+                <div v-else class="cw-block-download-content">
+                    <div class="cw-block-download-file-item-not-available">
+                        <span class="cw-block-file-info cw-block-file-icon-none">
+                            {{ $gettext('Datei ist nicht verfügbar') }}
+                        </span>
+                    </div>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <courseware-tabs>
+                    <courseware-tab :index="0" :name="$gettext('Datei')" :selected="true">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Überschrift') }}
+                                <input type="text" v-model="currentTitle" />
+                            </label>
+                            <label>
+                                {{ $gettext('Datei') }}
+                                <courseware-file-chooser v-model="currentFileId" @selectFile="updateCurrentFile" />
+                            </label>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab :index="1" :name="$gettext('Infobox')">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Infobox vor Download') }}
+                                <input type="text" v-model="currentInfo" />
+                            </label>
+                            <label>
+                                {{ $gettext('Infobox nach Download') }}
+                                <input type="text" v-model="currentSuccess" />
+                            </label>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab :index="2" :name="$gettext('Fortschritt')">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Fortschritt erst beim Herunterladen') }}
+                                <select v-model="currentGrade">
+                                    <option value="false">{{ $gettext('Nein') }}</option>
+                                    <option value="true">{{ $gettext('Ja') }}</option>
+                                </select>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Download-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-download-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentInfo: '',
+            currentSuccess: '',
+            currentGrade: '',
+            currentFileId: '',
+            currentFile: null,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            fileRefById: 'file-refs/byId',
+            urlHelper: 'urlHelper',
+            relatedTermOfUse: 'terms-of-use/related',
+            getUserDataById: 'courseware-user-data-fields/byId',
+        }),
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        info() {
+            return this.block?.attributes?.payload?.info;
+        },
+        success() {
+            return this.block?.attributes?.payload?.success;
+        },
+        grade() {
+            return this.block?.attributes?.payload?.grade;
+        },
+        fileId() {
+            return this.block?.attributes?.payload?.file_id;
+        },
+        userData() {
+            return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id });
+        },
+        userHasDownloaded() {
+            let downloaded = this.userData?.attributes?.payload?.downloaded;
+
+            if (downloaded === undefined) {
+                return false;
+            }
+
+            return downloaded;
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+        if (this.userProgress && this.userProgress.attributes.grade === 0 && !this.grade) {
+            this.userProgress = 1;
+        }
+    },
+    methods: {
+        ...mapActions({
+            loadFileRef: 'file-refs/loadById',
+            updateBlock: 'updateBlockInContainer',
+            updateUserDataFields: 'courseware-user-data-fields/update',
+        }),
+        initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentInfo = this.info;
+            this.currentFileId = this.fileId;
+            this.currentSuccess = this.success;
+            this.currentGrade = this.grade;
+            if (this.currentFileId !== '') {
+                this.loadFile();
+            }
+        },
+        async loadFile() {
+            const id = `${this.currentFileId}`;
+            const options = { include: 'terms-of-use' };
+            await this.loadFileRef({ id: id, options });
+            const fileRef = this.fileRefById({ id: id });
+            if (
+                fileRef &&
+                this.relatedTermOfUse({ parent: fileRef, relationship: 'terms-of-use' }).attributes[
+                    'download-condition'
+                ] === 0
+            ) {
+                this.updateCurrentFile({
+                    id: fileRef.id,
+                    name: fileRef.attributes.name,
+                    icon: this.getIcon(fileRef.attributes['mime-type']),
+                    download_url: this.urlHelper.getURL(
+                        'sendfile.php',
+                        { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
+                        true
+                    ),
+                });
+            }
+        },
+        updateCurrentFile(file) {
+            this.currentFile = file;
+            this.currentFileId = file.id;
+            if (!this.currentFile.icon) {
+                this.currentFile.icon = this.getIcon(file.mime_type);
+            }
+        },
+        getIcon(mimeType) {
+            let icon = 'file';
+            if (mimeType.includes('audio')) {
+                icon = 'audio';
+            }
+            if (mimeType.includes('image')) {
+                icon = 'pic';
+            }
+            if (mimeType.includes('video')) {
+                icon = 'video';
+            }
+            if (mimeType.includes('text')) {
+                icon = 'text';
+            }
+            if (mimeType.includes('pdf')) {
+                icon = 'pdf';
+            }
+            if (mimeType.includes('msword')) {
+                icon = 'word';
+            }
+            if (mimeType.includes('opendocument.text')) {
+                icon = 'word';
+            }
+            if (mimeType.includes('openxmlformats-officedocument. wordprocessingml.document')) {
+                icon = 'word';
+            }
+            if (mimeType.includes('msexcel')) {
+                icon = 'spreadsheet';
+            }
+            if (mimeType.includes('opendocument.spreadsheet')) {
+                icon = 'spreadsheet';
+            }
+            if (mimeType.includes('openxmlformats-officedocument. spreadsheetml.sheet')) {
+                icon = 'spreadsheet';
+            }
+            if (mimeType.includes('mspowerpoint')) {
+                icon = 'ppt';
+            }
+            if (mimeType.includes('zip')) {
+                icon = 'archive';
+            }
+
+            return icon;
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.title = this.currentTitle;
+            attributes.payload.info = this.currentInfo;
+            attributes.payload.success = this.currentSuccess;
+            attributes.payload.grade = this.currentGrade;
+            attributes.payload.file_id = this.currentFileId;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+        handleDownload() {
+            let data = {
+                id: this.userData.id,
+                type: 'courseware-user-data-fields',
+                relationships: {
+                    block: {
+                        data: {
+                            id: this.block.id,
+                            type: this.block.type,
+                        },
+                    },
+                },
+                attributes: {
+                    payload: {
+                        downloaded: true,
+                    },
+                },
+            };
+            this.updateUserDataFields(data);
+            this.userProgress = 1;
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/files.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareEmbedBlock.vue b/resources/vue/components/courseware/blocks/CoursewareEmbedBlock.vue
new file mode 100644
index 0000000..75ed4ee
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareEmbedBlock.vue
@@ -0,0 +1,252 @@
+<template>
+    <div class="cw-block cw-block-embed" ref="block">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="false"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
+                <template v-if="oembedData">
+                    <div
+                        v-if="oembedData.type === 'rich' || oembedData.type === 'video'"
+                        v-html="oembedData.html"
+                        class="cw-block-embed-iframe-wrapper"
+                        :style="{ height: contentHeight + 'px' }"
+                    ></div>
+
+                    <div v-if="oembedData.type === 'photo'" :style="{ height: contentHeight + 'px' }">
+                        <img :src="oembedData.url" />
+                    </div>
+
+                    <div v-if="oembedData.type === 'link' && oembedData.provider_name === 'DeviantArt'">
+                        <img :src="oembedData.fullsize_url" />
+                    </div>
+                    <div class="cw-block-embed-info">
+                        <span class="cw-block-embed-title">{{ oembedData.title }}</span>
+                        <span class="cw-block-embed-author-name">
+                            {{ $gettext('erstellt von') }}
+                            <a :href="oembedData.author_url" target="_blank">{{ oembedData.author_name }}</a></span
+                        >
+                        <span class="cw-block-embed-source">
+                            {{ $gettext('veröffentlicht auf') }}
+                            <a :href="oembedData.provider_url" target="_blank">{{ oembedData.provider_name }}</a></span
+                        >
+                    </div>
+                </template>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Überschrift') }}
+                        <input type="text" v-model="currentTitle" />
+                    </label>
+                    <label>
+                        {{ $gettext('Quelle') }}
+                        <select v-model="currentSource">
+                            <option v-for="(value, key) in endPoints" :key="key" :value="key">{{ key }}</option>
+                        </select>
+                    </label>
+                    <label>
+                        {{ $gettext('URL') }}
+                        <input type="text" v-model="currentUrl" />
+                    </label>
+                    <label v-if="currentSource === 'youtube'" class="col-1">
+                        {{ $gettext('Startpunkt') }}
+                        <input
+                            type="time"
+                            v-model="currentStartTime"
+                            step="1"
+                            min="00:00:00"
+                            max="24:00:00"
+                            @change="updateTime"
+                        />
+                    </label>
+                    <label v-if="currentSource === 'youtube'" class="col-1">
+                        {{ $gettext('Endpunkt') }}
+                        <input
+                            type="time"
+                            v-model="currentEndTime"
+                            step="1"
+                            :min="currentStartTime"
+                            max="24:00:00"
+                            @change="updateTime"
+                        />
+                    </label>
+                </form>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Embed-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-embed-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentSource: '',
+            currentUrl: '',
+            currentStartTime: '',
+            currentEndTime: '',
+
+            endPoints: {
+                audiomack: 'https://www.audiomack.com/oembed',
+                codepen: 'https://codepen.io/api/oembed',
+                codesandbox: 'https://codesandbox.io/oembed',
+                deviantart: 'https://backend.deviantart.com/oembed',
+                ethfiddle: 'https://ethfiddle.com/services/oembed/',
+                flickr: 'https://www.flickr.com/services/oembed/',
+                giphy: 'https://giphy.com/services/oembed',
+                kidoju: 'https://www.kidoju.com/api/oembed',
+                learningapps: 'https://learningapps.org/oembed.php',
+                sketchfab: 'https://sketchfab.com/oembed',
+                slideshare: 'https://www.slideshare.net/api/oembed/2',
+                soundcloud: 'https://soundcloud.com/oembed',
+                speakerdeck: 'https://speakerdeck.com/oembed.json',
+                sway: 'https://sway.com/api/v1.0/oembed',
+                'sway.office': 'https://sway.office.com/api/v1.0/oembed',
+                spotify: 'https://embed.spotify.com/oembed/',
+                vimeo: 'https://vimeo.com/api/oembed.json',
+                youtube: 'https://www.youtube.com/oembed',
+            },
+            oembedData: {},
+            contentHeight: 300,
+        };
+    },
+    computed: {
+        url() {
+            return this.block?.attributes?.payload?.url;
+        },
+        source() {
+            return this.block?.attributes?.payload?.source;
+        },
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        startTime() {
+            return this.block?.attributes?.payload?.starttime;
+        },
+        endTime() {
+            return this.block?.attributes?.payload?.endtime;
+        },
+        oembed() {
+            return this.block?.attributes?.payload?.oembed;
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+        window.addEventListener('resize', this.calcContentHeight);
+    },
+
+    created() {
+        STUDIP.eventBus.on('courseware:update-tab', (data) => {
+            this.recalculateContentHeight(data);
+        });
+        STUDIP.eventBus.on('courseware:update-collapsible', (data) => {
+            this.recalculateContentHeight(data);
+        });
+    },
+    destroyed() {
+        window.removeEventListener('resize', this.calcContentHeight);
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentSource = this.source;
+            this.currentUrl = this.url;
+            this.currentStartTime = this.startTime;
+            this.currentEndTime = this.endTime;
+            this.oembedData = this.oembed;
+            if (this.oembedData) {
+                this.updateTime();
+                this.calcContentHeight();
+            }
+        },
+        recalculateContentHeight(data) {
+            if (this.$parent._uid === data.uid) {
+                if (this.oembedData) {
+                    this.calcContentHeight();
+                }
+            }
+        },
+        addTimeData(data) {
+            if (this.currentSource === 'youtube') {
+                if (this.currentStartTime !== '') {
+                    let start = this.currentStartTime.split(':');
+                    let s = parseInt(start[0], 10) * 3600 + parseInt(start[1], 10) * 60 + parseInt(start[2], 10);
+                    let query = '?feature=oembed&start=' + s;
+                    if (this.currentEndTime !== '') {
+                        let end = this.currentEndTime.split(':');
+                        let e = parseInt(end[0], 10) * 3600 + parseInt(end[1], 10) * 60 + parseInt(end[2], 10);
+                        query = query + '&end=' + e;
+                    }
+                    data.html = data.html.replace('?feature=oembed', query);
+                }
+            }
+            return data;
+        },
+        updateTime() {
+            this.oembedData = this.addTimeData(this.oembedData);
+        },
+        validateCurrentSource() {
+            var validSource = false;
+            let view = this;
+            for (const key of Object.keys(this.endPoints)) {
+                if (view.currentUrl.includes(key)) {
+                    view.currentSource = key;
+                    validSource = true;
+                    break;
+                }
+            }
+
+            return validSource;
+        },
+        calcContentHeight() {
+            if (this.oembedData.height && this.oembedData.width) {
+                this.contentHeight =
+                    ((this.$refs.block.offsetWidth - 4) / this.oembedData.width) * this.oembedData.height;
+            }
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.title = this.currentTitle;
+            attributes.payload.url = this.currentUrl;
+            attributes.payload.source = this.currentSource;
+            attributes.payload.starttime = this.currentStartTime;
+            attributes.payload.endtime = this.currentEndTime;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/embed.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareErrorBlock.vue b/resources/vue/components/courseware/blocks/CoursewareErrorBlock.vue
new file mode 100644
index 0000000..39860ea
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareErrorBlock.vue
@@ -0,0 +1,52 @@
+<template>
+    <div class="cw-block cw-block-error">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :deleteOnly="true"
+            :isTeacher="isTeacher"
+            :preview="false"
+            :defaultGrade="false"
+        >
+            <template #content>
+                <div class="cw-block-error-content">
+                    <courseware-companion-box 
+                        mood="sad"
+                        :msgCompanion="errorMessage"
+                    >
+                    </courseware-companion-box>
+                </div>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+
+export default {
+    name: 'courseware-error-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    computed: {
+        originalBlockType() {
+            return this.block?.attributes?.payload?.original_block_type;
+        },
+        errorMessage() {
+            let message = '<b>'
+            message += this.$gettext('Es ist ein Fehler aufgetretten! Der Block-Typ dieses Blocks ist nicht verfügbar.');
+            message += '</b><br>'
+            message += 'block_type: ' + this.originalBlockType + ' not found';
+
+            return message;
+        }
+    },
+
+};
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue b/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue
new file mode 100644
index 0000000..6bbc48d
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue
@@ -0,0 +1,377 @@
+<template>
+    <div class="cw-block cw-block-folder">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
+                <div v-if="isHomework" class="cw-block-folder-info">
+                    <p>
+                        {{
+                            $gettext(
+                                'Dieser Ordner ist ein Hausaufgabenordner. Es können nur Dateien eingestellt werden.'
+                            )
+                        }}
+                    </p>
+                    <p v-if="!isTeacher">
+                        {{ $gettext('Sie selbst haben folgende Dateien in diesen Ordner eingestellt') }}:
+                    </p>
+                </div>
+                <ul class="cw-block-folder-list">
+                    <li v-for="file in files" :key="file.id" class="cw-block-folder-file-item">
+                        <a
+                            v-if="downloadEnabled"
+                            target="_blank"
+                            :download="file.attributes.name"
+                            :title="$gettext('Datei herunterladen')"
+                            :href="file.meta['download-url']"
+                        >
+                            <span
+                                class="cw-block-file-info"
+                                :class="['cw-block-file-icon-' + getIcon(file.attributes['mime-type'])]"
+                            >
+                                {{ file.attributes.name }}
+                            </span>
+                            <div v-if="isTeacher && isHomework" class="cw-block-file-details">
+                                <span class="cw-block-file-owner">
+                                    {{ file.relationships.owner.meta.name }}
+                                </span>
+                                <span class="cw-block-file-mkdate">
+                                    {{ getFormattedDate(file.attributes.mkdate) }}
+                                </span>
+                            </div>
+                        </a>
+                        <template v-else>
+                            <span
+                                class="cw-block-file-info download-disabled"
+                                :class="['cw-block-file-icon-' + getIcon(file.attributes['mime-type'])]"
+                            >
+                                {{ file.attributes.name }}
+                            </span>
+                            <div class="cw-block-file-details">
+                                <span class="cw-block-file-mkdate">
+                                    {{ getFormattedDate(file.attributes.mkdate) }}
+                                </span>
+                            </div>
+                        </template>
+                    </li>
+                    <li v-if="files.length === 0">
+                        <span class="cw-block-file-info cw-block-file-icon-empty">
+                            {{ $gettext('Dieser Ordner ist leer') }}
+                        </span>
+                    </li>
+                </ul>
+                <div v-if="uploadEnabled" class="cw-block-folder-upload">
+                    <form class="default" @submit.prevent="">
+                        <label>
+                            {{ $gettext('Dateien zum Hochladen auswählen') }}
+                            <input class="cw-file-input" ref="uploadFile" type="file" @change="displayTermSelector" />
+                            <button class="button" @click="uploadFile">
+                                {{ $gettext('Datei hochladen') }}
+                            </button>
+                        </label>
+                    </form>
+                    <studip-dialog
+                        v-if="showTermSelector"
+                        width="780"
+                        height="510"
+                        :title="$gettext('Lizenz auswählen')"
+                        :confirmText="$gettext('Speichern')"
+                        confirmClass="accept"
+                        :closeText="$gettext('Lizenzauswahl abbrechen')"
+                        closeClass="cancel"
+                        @close="showTermSelector = false"
+                        @confirm="selectTerm"
+                    >
+                        <template v-slot:dialogContent>
+                            <form class="default" @submit.prevent="">
+                                <div style="margin-bottom: 1ex">
+                                    {{
+                                        $gettext(
+                                            'Bereitgestellte Dateien können heruntergeladen und ggf. weiterverbreitet werden. Dabei ist das Urheberrecht sowohl beim Hochladen der Datei als auch bei der Nutzung zu beachten. Bitte geben Sie daher an, um welche Art von Bereitstellung es sich handelt. Diese Angabe dient mehreren Zwecken: Beim Herunterladen wird ein Hinweis angezeigt, welche Nutzung der Datei zulässig ist. Beim Hochladen stellt die Angabe eine Entscheidungshilfe dar, damit Sie sichergehen können, dass die Datei tatsächlich bereitgestellt werden darf.'
+                                        )
+                                    }}
+                                </div>
+                                <fieldset class="select_terms_of_use">
+                                    <template v-for="term in termsOfUse">
+                                        <input
+                                            type="radio"
+                                            name="content_terms_of_use_id"
+                                            :value="term.id"
+                                            v-model="selectedTerm"
+                                            :id="'content_terms_of_use-' + term.id"
+                                            :checked="selectedTerm === term.id"
+                                            :aria-description="term.description"
+                                            :key="term.id + '_input'"
+                                        />
+                                        <label @click="selectedTerm = term.id" :key="term.id + 'label'">
+                                            <div class="icon">
+                                                <studip-icon :shape="term.attributes.icon" :size="32" />
+                                            </div>
+                                            <div class="text">
+                                                {{ term.attributes.name }}
+                                            </div>
+                                            <studip-icon shape="arr_1down" :size="24" class="arrow" />
+                                            <studip-icon shape="check-circle" :size="24" class="check" />
+                                        </label>
+                                        <div class="terms_of_use_description" :key="term.id + '_description'">
+                                            <div class="description">
+                                                {{ term.attributes.description }}
+                                            </div>
+                                        </div>
+                                    </template>
+                                </fieldset>
+                            </form>
+                        </template>
+                    </studip-dialog>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Überschrift') }}
+                        <input type="text" v-model="currentTitle" />
+                    </label>
+                    <label>
+                        {{ $gettext('Ordner') }}
+                        <courseware-folder-chooser v-model="currentFolderId" allowUserFolders allowHomeworkFolders />
+                    </label>
+                </form>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Dateiordner-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-folder-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentFolderId: '',
+            currentFolderType: '',
+            showTermSelector: false,
+            selectedTerm: null,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            folderById: 'folders/byId',
+            termsOfUse: 'terms-of-use/all',
+        }),
+        folderType() {
+            return this.block?.attributes?.payload?.type;
+        },
+        storedFolderType() {
+            return this.block?.attributes?.payload?.folder_type;
+        },
+        folderTypeHasChanged() {
+            return this.folderType === this.storedFolderType;
+        },
+        folderId() {
+            return this.block?.attributes?.payload?.folder_id;
+        },
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        files() {
+            return this.block?.attributes?.payload?.files;
+        },
+        isHomework() {
+            return this.folderType === 'HomeworkFolder';
+        },
+        uploadEnabled() {
+            return !this.isTeacher && this.isHomework;
+        },
+        downloadEnabled() {
+            return this.isTeacher || !this.isHomework;
+        },
+    },
+    async mounted() {
+        await this.loadTermsOfUse();
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            loadFolder: 'folders/loadById',
+            loadBlock: 'courseware-blocks/loadById',
+            updateBlock: 'updateBlockInContainer',
+            createFile: 'createFile',
+            companionWarning: 'companionWarning',
+            companionSuccess: 'companionSuccess',
+            companionError: 'companionError',
+            loadTermsOfUse: 'terms-of-use/loadAll',
+        }),
+        async initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentFolderId = this.folderId;
+            if (this.$refs?.uploadFile) {
+                this.$refs.uploadFile.value = null;
+            }
+            this.selectedTerm = this.getDefaultTerm();
+        },
+        async setCurrentFolderType() {
+            await this.loadFolder({ id: this.currentFolderId });
+            const folder = this.folderById({ id: this.currentFolderId });
+            this.currentFolderType = folder?.attributes['folder-type'];
+        },
+        getIcon(mimeType) {
+            let icon = 'file';
+            if (mimeType.includes('audio')) {
+                icon = 'audio';
+            }
+            if (mimeType.includes('image')) {
+                icon = 'pic';
+            }
+            if (mimeType.includes('video')) {
+                icon = 'video';
+            }
+            if (mimeType.includes('text')) {
+                icon = 'text';
+            }
+            if (mimeType.includes('pdf')) {
+                icon = 'pdf';
+            }
+            if (mimeType.includes('msword')) {
+                icon = 'word';
+            }
+            if (mimeType.includes('opendocument.text')) {
+                icon = 'word';
+            }
+            if (mimeType.includes('openxmlformats-officedocument. wordprocessingml.document')) {
+                icon = 'word';
+            }
+            if (mimeType.includes('msexcel')) {
+                icon = 'spreadsheet';
+            }
+            if (mimeType.includes('opendocument.spreadsheet')) {
+                icon = 'spreadsheet';
+            }
+            if (mimeType.includes('openxmlformats-officedocument. spreadsheetml.sheet')) {
+                icon = 'spreadsheet';
+            }
+            if (mimeType.includes('mspowerpoint')) {
+                icon = 'ppt';
+            }
+            if (mimeType.includes('zip')) {
+                icon = 'archive';
+            }
+
+            return icon;
+        },
+        getFormattedDate(unformattedDate) {
+            const date = new Date(unformattedDate);
+            const localeDate = date.toLocaleDateString('de-DE', {
+                year: 'numeric',
+                month: '2-digit',
+                day: '2-digit',
+            });
+
+            return `${localeDate} ${date.getHours()}:${(date.getMinutes() < 10 ? '0' : '') + date.getMinutes()}:${
+                (date.getSeconds() < 10 ? '0' : '') + date.getSeconds()
+            }`;
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.title = this.currentTitle;
+            attributes.payload.folder_id = this.currentFolderId;
+            attributes.payload.type = this.currentFolderType;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+        displayTermSelector() {
+            this.showTermSelector = true;
+        },
+        selectTerm() {
+            this.showTermSelector = false;
+            this.uploadFile();
+        },
+        async uploadFile() {
+            const userFile = this.$refs?.uploadFile?.files[0];
+            if (!userFile) {
+                this.companionWarning({
+                    info: this.$gettext('Bitte wählen Sie eine Datei aus.'),
+                });
+                return;
+            }
+
+            let file = {
+                attributes: {
+                    name: userFile.name.replace(/\s/g, '_'),
+                },
+                relationships: {
+                    'terms-of-use': {
+                        data: {
+                            id: this.selectedTerm,
+                        },
+                    },
+                },
+            };
+            let fileObj = await this.createFile({
+                file: file,
+                filedata: userFile,
+                folder: { id: this.currentFolderId },
+            });
+            if (fileObj && fileObj.type === 'file-refs') {
+                this.companionSuccess({
+                    info: this.$gettext('Die Datei wurde erfolgreich im Dateibereich abgelegt.'),
+                });
+            } else {
+                if (this.folderType !== 'HomeworkFolder') {
+                    this.companionError({
+                        info: this.$gettext('Es ist ein Fehler aufgetretten.'),
+                    });
+                }
+            }
+            this.reload();
+        },
+        async reload() {
+            await this.loadBlock({ id: this.block.id });
+            this.initCurrentData();
+        },
+        getDefaultTerm() {
+            const defaultTerm = this.termsOfUse.filter((term) => term.attributes['is-default'])[0];
+            if (defaultTerm) {
+                return defaultTerm.id;
+            }
+            return null;
+        },
+    },
+    watch: {
+        currentFolderId() {
+            if (this.canEdit) {
+                this.setCurrentFolderType();
+            }
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/files.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue b/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue
new file mode 100644
index 0000000..40bc33c
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue
@@ -0,0 +1,400 @@
+<template>
+    <div class="cw-block cw-block-gallery">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="showEdit"
+            @storeEdit="storeBlock"
+            @closeEdit="closeEdit"
+        >
+            <template #content>
+                <template v-if="files.length !== 0">
+                    <div
+                        v-if="currentLayout === 'carousel'"
+                        class="cw-block-gallery-content"
+                        :style="{ height: `${currentHeight}px` }"
+                    >
+                        <div
+                            v-for="(image, index) in files"
+                            :key="image.id"
+                            ref="images"
+                            class="cw-block-gallery-slides cw-block-gallery-fade"
+                        >
+                            <div class="cw-block-gallery-number-text">{{ index + 1 }} / {{ files.length }}</div>
+                            <img
+                                :src="image.meta['download-url']"
+                                :style="{ height: `${currentHeight}px` }"
+                                @load="
+                                    if (files.length - 1 === index) {
+                                        startGallery();
+                                    }
+                                "
+                            />
+                            <div
+                                class="cw-block-gallery-file-description"
+                                :class="{ 'show-on-hover': currentMouseoverFileNames === 'true' }"
+                            >
+                                <p v-if="currentShowFileNames === 'true'">{{ image?.attributes?.name }}</p>
+                                <p v-if="currentShowFileDescription === 'true'">{{ image?.attributes?.description }}</p>
+                            </div>
+                        </div>
+                        <div v-if="currentNav === 'true'">
+                            <button
+                                class="cw-block-gallery-prev"
+                                :title="$gettext('Vorheriges Bild')"
+                                @click="plusSlides(-1)"
+                            ></button>
+                            <button
+                                class="cw-block-gallery-next"
+                                :title="$gettext('Nächstes Bild')"
+                                @click="plusSlides(1)"
+                            ></button>
+                        </div>
+                    </div>
+                    <div v-if="currentLayout === 'grid'" class="cw-block-gallery-content">
+                        <div class="cw-block-gallery-grid">
+                            <figure v-for="image in files" :key="image.id" :style="{ 'max-width': gridWidth }">
+                                <img :src="image.meta['download-url']" :title="image?.attributes?.name" />
+                                <figcaption v-if="showDescription">
+                                    <p v-if="currentShowFileNames === 'true'" class="cw-block-gallery-grid-file-name">
+                                        {{ image?.attributes?.name }}
+                                    </p>
+                                    <p
+                                        v-if="currentShowFileDescription === 'true'"
+                                        class="cw-block-gallery-grid-file-description"
+                                    >
+                                        {{ image?.attributes?.description }}
+                                    </p>
+                                </figcaption>
+                            </figure>
+                        </div>
+                    </div>
+                </template>
+            </template>
+            <template v-if="canEdit" #edit>
+                <courseware-tabs>
+                    <courseware-tab :index="0" :name="$gettext('Einstellungen')" :selected="true">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Layout') }}
+                                <select v-model="currentLayout">
+                                    <option value="carousel">{{ $gettext('Karussell') }}</option>
+                                    <option value="grid">{{ $gettext('Gitter') }}</option>
+                                </select>
+                            </label>
+                            <label>
+                                {{ $gettext('Ordner') }}
+                                <courseware-folder-chooser v-model="currentFolderId" allowUserFolders />
+                            </label>
+                            <label v-if="currentLayout === 'carousel'">
+                                {{ $gettext('Höhe') }}
+                                <input type="number" min="0" max="800" v-model="currentHeight" />
+                            </label>
+                            <label v-if="currentLayout === 'grid'">
+                                {{ $gettext('Gitter-Spalten') }}
+                                <select v-model="currentCols">
+                                    <option :value="100">1</option>
+                                    <option :value="50">2</option>
+                                    <option :value="33">3</option>
+                                    <option :value="25">4</option>
+                                    <option :value="20">5</option>
+                                </select>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab :index="1" :name="$gettext('Beschreibung')">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Dateinamen anzeigen') }}
+                                <select v-model="currentShowFileNames">
+                                    <option value="true">{{ $gettext('Ja') }}</option>
+                                    <option value="false">{{ $gettext('Nein') }}</option>
+                                </select>
+                            </label>
+                            <label>
+                                {{ $gettext('Dateibeschreibung anzeigen') }}
+                                <select v-model="currentShowFileDescription">
+                                    <option value="true">{{ $gettext('Ja') }}</option>
+                                    <option value="false">{{ $gettext('Nein') }}</option>
+                                </select>
+                            </label>
+                            <label v-if="showDescription && currentLayout === 'carousel'">
+                                {{ $gettext('Beschreibung erscheint bei Mouseover') }}
+                                <studip-tooltip-icon
+                                    :text="
+                                        $gettext(
+                                            'Der Beschreibungstext wird angezeigt, wenn Sie den Mauszeiger über das Bild bewegen.'
+                                        )
+                                    "
+                                />
+                                <select v-model="currentMouseoverFileNames">
+                                    <option value="true">{{ $gettext('Ja') }}</option>
+                                    <option value="false">{{ $gettext('Nein') }}</option>
+                                </select>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab v-if="currentLayout === 'carousel'" :index="2" :name="$gettext('Autoplay')">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Autoplay') }}
+                                <select v-model="currentAutoplay">
+                                    <option value="true">{{ $gettext('Ja') }}</option>
+                                    <option value="false">{{ $gettext('Nein') }}</option>
+                                </select>
+                            </label>
+                            <label v-if="currentAutoplay === 'true'">
+                                {{ $gettext('Autoplay Timer in Sekunden') }}
+                                <input type="number" min="1" max="60" v-model="currentAutoplayTimer" />
+                            </label>
+                            <label v-if="currentAutoplay === 'true'">
+                                {{ $gettext('Navigation') }}
+                                <select v-model="currentNav">
+                                    <option value="true">{{ $gettext('Ja') }}</option>
+                                    <option value="false">{{ $gettext('Nein') }}</option>
+                                </select>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Galerie-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-gallery-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentFolderId: '',
+            currentLayout: '',
+            currentAutoplay: '',
+            currentNav: '',
+            currentHeight: '',
+            currentCols: 33,
+            currentShowFileNames: '',
+            currentShowFileDescription: '',
+            currentMouseoverFileNames: '',
+            currentAutoplayTimer: '',
+            editModeFiles: [],
+            slideIndex: 0,
+            editMode: false,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            urlHelper: 'urlHelper',
+            relatedFileRefs: 'file-refs/related',
+            relatedTermOfUse: 'terms-of-use/related',
+        }),
+        folderId() {
+            return this.block?.attributes?.payload?.folder_id;
+        },
+        layout() {
+            return this.block?.attributes?.payload?.layout ?? 'carousel';
+        },
+        autoplay() {
+            return this.block?.attributes?.payload?.autoplay;
+        },
+        autoplayTimer() {
+            return this.block?.attributes?.payload?.autoplay_timer;
+        },
+        nav() {
+            return this.block?.attributes?.payload?.nav;
+        },
+        height() {
+            return this.block?.attributes?.payload?.height;
+        },
+        cols() {
+            return this.block?.attributes?.payload?.cols;
+        },
+        showFileNames() {
+            return this.block?.attributes?.payload?.show_filenames;
+        },
+        showFileDescription() {
+            return this.block?.attributes?.payload?.show_description ?? 'false';
+        },
+        mouseoverFileNames() {
+            return this.block?.attributes?.payload?.mouseover_filenames ?? 'false';
+        },
+        files() {
+            if (!this.editMode) {
+                return this.block?.attributes?.payload?.files;
+            }
+            return this.editModeFiles;
+        },
+        gridWidth() {
+            return 'calc(' + this.currentCols + '% - 8px)';
+        },
+        showDescription() {
+            return this.currentShowFileNames === 'true' || this.currentShowFileDescription === 'true';
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            loadRelatedFileRefs: 'file-refs/loadRelated',
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            this.currentFolderId = this.folderId;
+            this.currentLayout = this.layout;
+            this.currentAutoplay = this.autoplay;
+            this.currentAutoplayTimer = this.autoplayTimer;
+            this.currentNav = this.nav;
+            this.currentHeight = this.height;
+            this.currentCols = this.cols;
+            this.currentShowFileNames = this.showFileNames;
+            this.currentShowFileDescription = this.showFileDescription;
+            this.currentMouseoverFileNames = this.mouseoverFileNames;
+        },
+        startGallery() {
+            this.slideIndex = 0;
+            this.showSlides(0);
+            if (this.currentAutoplay === 'true') {
+                this.playSlides();
+            }
+        },
+        async getFolderFiles() {
+            const parent = { type: 'folders', id: `${this.currentFolderId}` };
+            const relationship = 'file-refs';
+            const options = { include: 'terms-of-use' };
+            await this.loadRelatedFileRefs({ parent, relationship, options });
+
+            const files = this.relatedFileRefs({ parent, relationship });
+            this.processFiles(files);
+        },
+        processFiles(files) {
+            this.editModeFiles = files
+                .filter((file) => {
+                    if (
+                        this.relatedTermOfUse({ parent: file, relationship: 'terms-of-use' }).attributes[
+                            'download-condition'
+                        ] !== 0
+                    ) {
+                        return false;
+                    }
+                    if (!file.attributes['mime-type'].includes('image')) {
+                        return false;
+                    }
+
+                    return true;
+                })
+                .map((file) => ({
+                    id: file.id,
+                    attributes: {
+                        name: file.attributes.name,
+                        description: file.attributes.description,
+                    },
+                    meta: {
+                        'download-url': this.urlHelper.getURL(
+                            'sendfile.php',
+                            { type: 0, file_id: file.id, file_name: file.attributes.name },
+                            true
+                        ),
+                    },
+                }));
+        },
+        showEdit() {
+            this.editMode = true;
+            this.initCurrentData();
+        },
+        closeEdit() {
+            this.editMode = false;
+            this.initCurrentData();
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.folder_id = this.currentFolderId;
+            attributes.payload.layout = this.currentLayout;
+            attributes.payload.autoplay = this.currentAutoplay;
+            attributes.payload.autoplay_timer = this.currentAutoplayTimer;
+            attributes.payload.nav = this.currentNav;
+            attributes.payload.height = this.currentHeight;
+            attributes.payload.cols = this.currentCols;
+            attributes.payload.show_filenames = this.currentShowFileNames;
+            attributes.payload.show_description = this.currentShowFileDescription;
+            attributes.payload.mouseover_filenames = this.currentMouseoverFileNames;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+        plusSlides(n) {
+            this.showSlides((this.slideIndex += n));
+        },
+        showSlides(n) {
+            let slides = this.$refs.images;
+            if (slides === undefined) {
+                return false;
+            }
+            if (n > slides.length - 1) {
+                this.slideIndex = 0;
+            }
+            if (n < 0) {
+                this.slideIndex = slides.length - 1;
+            }
+            slides.forEach((slide) => {
+                slide.style.display = 'none';
+            });
+            slides[this.slideIndex].style.display = 'block';
+        },
+        playSlides() {
+            let slides = this.$refs.images;
+            slides.forEach((slide) => {
+                slide.style.display = 'none';
+            });
+            if (this.slideIndex > slides.length - 1) {
+                this.slideIndex = 0;
+            }
+            if (slides[this.slideIndex]) {
+                slides[this.slideIndex].style.display = 'block';
+            }
+            this.slideIndex++;
+            if (this.currentAutoplay === 'true') {
+                setTimeout(this.playSlides, this.currentAutoplayTimer * 1000);
+            }
+        },
+    },
+    watch: {
+        currentFolderId() {
+            this.getFolderFiles();
+        },
+        currentAutoplay(value) {
+            if (value === 'false') {
+                this.currentNav = 'true';
+            }
+        },
+        currentAutoplayTimer(value) {
+            if (value > 60 || value < 1) {
+                this.currentAutoplayTimer = '2';
+            }
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/gallery.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue b/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue
new file mode 100644
index 0000000..c7dd6b5
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue
@@ -0,0 +1,631 @@
+<template>
+    <div class="cw-block cw-block-headline">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeText"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div
+                    class="cw-block-headline-content"
+                    :class="[
+                        currentStyle,
+                        currentHeight,
+                        hasGradient ? currentGradient : ''
+                    ]"
+                    :style="headlineStyle"
+                >
+                    <div
+                        class="cw-block-headline-iconbox"
+                        :class="['border-' + currentIconColor, currentHeight]"
+                    >
+                        <div
+                            class="icon-layer"
+                            :class="['icon-' + currentIconColor + '-' + currentIcon, currentHeight]"
+                        >
+                        </div>
+                    </div>
+                    <div
+                        class="cw-block-headline-textbox"
+                        :class="['border-' + currentIconColor, currentHeight]"
+                        :style="currentStyle === 'ribbon' ? headlineTextboxStyle.rgba : {}"
+                    >
+                        <div class="cw-block-headline-title" :style="currentStyle === 'vertical' ? headlineTextboxStyle.hex : {}">
+                            <h1 :style="textStyle">{{ currentTitle }}</h1>
+                        </div>
+                        <div v-show="hasSubtitle && subtitleIsSet" class="cw-block-headline-subtitle" :style="currentStyle === 'vertical' ? headlineTextboxStyle.rgba : {}">
+                            <h2 :style="textStyle">{{ currentSubtitle }}</h2>
+                        </div>
+                        <div v-show="hasSecondSubtitle" class="cw-block-headline-second-subtitle">
+                            <h2 :style="textStyle">{{ currentSecondSubtitle }}</h2>
+                        </div>
+                    </div>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <courseware-tabs>
+                    <courseware-tab
+                        :index="0"
+                        :name="$gettext('Layout')"
+                        :selected="true"
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Typ') }}
+                                <select v-model="currentStyle">
+                                    <option value="heavy">{{ $gettext('Große Schrift') }}</option>
+                                    <option value="ribbon">{{ $gettext('Band') }}</option>
+                                    <option value="vertical">{{ $gettext('Vertikal') }}</option>
+                                    <option value="bigicon_top">{{ $gettext('Großes Icon oben') }}</option>
+                                    <option value="bigicon_before">{{ $gettext('Großes Icon davor') }}</option>
+                                    <option value="icon_top_lines">{{ $gettext('Icon oben mit Linien') }}</option>
+                                    <option value="skew_text">{{ $gettext('Schief') }}</option>
+                                </select>
+                            </label>
+                            <label>
+                                {{ $gettext('Höhe') }}
+                                <select v-model="currentHeight">
+                                    <option value="full">{{ $gettext('Voll') }}</option>
+                                    <option value="half">{{ $gettext('Halb') }}</option>
+                                    <option value="quarter">{{ $gettext('Viertel') }}</option>
+                                </select>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab
+                        :index="1"
+                        :name="$gettext('Inhalt')"
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Haupttitel') }}
+                                <input type="text" v-model="currentTitle" />
+                            </label>
+                            <label v-if="hasSubtitle">
+                                {{ $gettext('Untertitel') }}
+                                <input type="text" v-model="currentSubtitle" />
+                            </label>
+                            <label v-if="hasSecondSubtitle">
+                                {{ $gettext('2. Untertitel') }}
+                                <input type="text" v-model="currentSecondSubtitle" />
+                            </label>
+                            <label>
+                                {{ $gettext('Textfarbe') }}
+                                <studip-select
+                                    :options="colors"
+                                    label="hex"
+                                    :reduce="color => color.hex"
+                                    :clearable="false"
+                                    v-model="currentTextColor"
+                                >
+                                    <template #open-indicator="selectAttributes">
+                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
+                                    </template>
+                                    <template #no-options>
+                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                                    </template>
+                                    <template #selected-option="{name, hex}">
+                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
+                                    </template>
+                                    <template #option="{name, hex}">
+                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
+                                    </template>
+                                </studip-select>
+                            </label>
+                            <label v-if="hasTextBackgroundColor">
+                                {{ $gettext('Texthintergrundfarbe') }}
+                                <studip-select
+                                    :options="colors"
+                                    label="hex"
+                                    :reduce="color => color.hex"
+                                    :clearable="false"
+                                    v-model="currentTextBackgroundColor"
+                                >
+                                    <template #open-indicator="selectAttributes">
+                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
+                                    </template>
+                                    <template #no-options>
+                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                                    </template>
+                                    <template #selected-option="{name, hex}">
+                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
+                                    </template>
+                                    <template #option="{name, hex}">
+                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
+                                    </template>
+                                </studip-select>
+                            </label>
+                            <template v-if="hasIcon">
+                                <label>
+                                    {{ $gettext('Icon') }}
+                                    <studip-select :clearable="false" :options="icons" v-model="currentIcon">
+                                        <template #open-indicator="selectAttributes">
+                                            <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
+                                        </template>
+                                        <template #no-options>
+                                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                                        </template>
+                                        <template #selected-option="option">
+                                            <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
+                                        </template>
+                                        <template #option="option">
+                                            <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
+                                        </template>
+                                    </studip-select>
+                                </label>
+                                <label>
+                                    {{ $gettext('Icon-Farbe') }}
+                                    <studip-select
+                                        :options="iconColors"
+                                        label="name"
+                                        :reduce="iconColor => iconColor.class"
+                                        :clearable="false"
+                                        v-model="currentIconColor"
+                                    >
+                                        <template #open-indicator="selectAttributes">
+                                            <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
+                                        </template>
+                                        <template #no-options>
+                                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                                        </template>
+                                        <template #selected-option="{name, hex}">
+                                            <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
+                                        </template>
+                                        <template #option="{name, hex}">
+                                            <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
+                                        </template>
+                                    </studip-select>
+                                </label>
+                            </template>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab
+                        :index="2"
+                        :name="$gettext('Hintergrund')"
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Hintergrundtyp') }}
+                                <select v-model="currentBackgroundType">
+                                    <option value="color">{{ $gettext('Farbe') }}</option>
+                                    <option value="gradient">{{ $gettext('Farbverlauf') }}</option>
+                                    <option value="image">{{ $gettext('Bild') }}</option>
+                                    <option value="structural-element-image">{{ $gettext('Seiten-Bild') }}</option>
+                                </select>
+                            </label>
+                            <label  v-if="currentBackgroundType === 'color'">
+                                {{ $gettext('Hintergrundfarbe') }}
+                                <studip-select
+                                    :options="colors"
+                                    label="hex"
+                                    :reduce="color => color.hex"
+                                    v-model="currentBackgroundColor"
+                                    :clearable="false"
+                                >
+                                    <template #open-indicator="selectAttributes">
+                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
+                                    </template>
+                                    <template #no-options>
+                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                                    </template>
+                                    <template #selected-option="{name, hex}">
+                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
+                                    </template>
+                                    <template #option="{name, hex}">
+                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
+                                    </template>
+                                </studip-select>
+                            </label>
+                            <label v-if="currentBackgroundType === 'image'">
+
+                                <div>{{ $gettext('Hintergrundbild') }}</div>
+
+                                <template v-if="currentBackgroundImageId">
+                                    <template v-if="currentBackgroundImageType === 'file-refs'">
+                                        <StockImageThumbnail :url="currentBackgroundURL" width="8rem" contain />
+                                    </template>
+                                    <template v-if="currentBackgroundImageType === 'stock-images'">
+                                        <StockImageSelectableImageCard
+                                            :stock-image="selectedStockImage"
+                                            v-if="selectedStockImage"
+                                            />
+                                    </template>
+                                    <label>
+                                        <button class="button" type="button" @click="onClickRemoveBackgroundImage">
+                                            {{ $gettext('Bild entfernen') }}
+                                        </button>
+                                    </label>
+                                </template>
+
+                                <template v-if="!currentBackgroundImageId">
+                                    <courseware-file-chooser
+                                        v-model="currentBackgroundImageId"
+                                        :isImage="true"
+                                        @selectFile="onSelectFile"
+                                        />
+                                    <div style="margin-block-start: 1em">{{ $gettext('oder') }}</div>
+                                    <button class="button" type="button" @click="showStockImageSelector = true">
+                                        {{ $gettext('Aus dem Bilderpool auswählen') }}
+                                    </button>
+                                    <StockImageSelector
+                                        v-if="showStockImageSelector"
+                                        @close="showStockImageSelector = false"
+                                        @select="onSelectStockImage"
+                                        />
+                                </template>
+
+                            </label>
+                            <label v-if="currentBackgroundType === 'gradient'">
+                                {{ $gettext('Hintergrundfarbverlauf') }}
+                                <select v-model="currentGradient">
+                                    <option value="color-blue">{{ $gettext('Blau') }}</option>
+                                    <option value="color-purple">{{ $gettext('Purpur') }}</option>
+                                    <option value="color-violet">{{ $gettext('Violet') }}</option>
+                                    <option value="color-red">{{ $gettext('Rot') }}</option>
+                                    <option value="color-orange">{{ $gettext('Orange') }}</option>
+                                    <option value="color-yellow">{{ $gettext('Gelb') }}</option>
+                                    <option value="color-green">{{ $gettext('Grün') }}</option>
+                                    <option value="color-petrol">{{ $gettext('Petrol') }}</option>
+                                    <option value="color-grey">{{ $gettext('Grau') }}</option>
+                                    <option value="paper">{{ $gettext('Papier') }}</option>
+                                    <option value="blueprint">{{ $gettext('Blueprint') }}</option>
+                                    <option value="grid">{{ $gettext('Gitter') }}</option>
+                                    <option value="blue-bar">{{ $gettext('Blaue Balken') }}</option>
+                                    <option value="green-bar">{{ $gettext('Grüne Balken') }}</option>
+                                    <option value="faded-sun">{{ $gettext('Faded Sun') }}</option>
+                                    <option value="romantic-sun">{{ $gettext('Romantic Sun') }}</option>
+                                    <option value="bright-rain">{{ $gettext('Bright Rain') }}</option>
+                                    <option value="soft-weather">{{ $gettext('Soft Weather') }}</option>
+                                </select>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+
+            </template>
+            <template #info>{{ $gettext('Informationen zum Blickfang-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import contentIconsMixin from '@/vue/mixins/courseware/content-icons.js';
+import StockImageSelector from '../../stock-images/SelectorDialog.vue';
+import StockImageSelectableImageCard from '../../stock-images/SelectableImageCard.vue';
+import StockImageThumbnail from '../../stock-images/Thumbnail.vue';
+
+import { mapGetters, mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-headline-block',
+    mixins: [blockMixin, colorMixin, contentIconsMixin],
+    components: Object.assign(BlockComponents, {
+        StockImageSelector,
+        StockImageSelectableImageCard,
+        StockImageThumbnail,
+    }),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentSubtitle: '',
+            currentSecondSubtitle: '',
+            currentStyle: '',
+            currentHeight: '',
+            currentBackgroundColor: '',
+            currentTextColor: '',
+            currentTextBackgroundColor: '',
+            currentIcon: '',
+            currentIconColor: '',
+            currentBackgroundType: '',
+            currentBackgroundImageId: '',
+            currentBackgroundImageType: '',
+            currentBackgroundImage: {},
+            currentBackgroundURL: '',
+            currentGradient: '',
+            showStockImageSelector: false,
+            selectedStockImage: null,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            fileRefById: 'file-refs/byId',
+            stockImageById: 'stock-images/byId',
+            urlHelper: 'urlHelper',
+            relatedTermOfUse: 'terms-of-use/related',
+            currentStructuralElementImageURL: 'currentStructuralElementImageURL'
+        }),
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        subtitle() {
+            return this.block?.attributes?.payload?.subtitle;
+        },
+        secondSubtitle() {
+            return this.block?.attributes?.payload?.second_subtitle;
+        },
+        style() {
+            return this.block?.attributes?.payload?.style;
+        },
+        height() {
+            return this.block?.attributes?.payload?.height;
+        },
+        backgroundColor() {
+            return this.block?.attributes?.payload?.background_color;
+        },
+        gradient() {
+            return this.block?.attributes?.payload?.gradient;
+        },
+        textColor() {
+            return this.block?.attributes?.payload?.text_color;
+        },
+        textBackgroundColor() {
+            return this.block?.attributes?.payload?.text_background_color ?? '#000000';
+        },
+        icon() {
+            return this.block?.attributes?.payload?.icon;
+        },
+        iconColor() {
+            return this.block?.attributes?.payload?.icon_color;
+        },
+        backgroundImageId() {
+            return this.block?.attributes?.payload?.background_image_id;
+        },
+        backgroundImageType() {
+            return this.block?.attributes?.payload?.background_image_type;
+        },
+        backgroundImage() {
+            return this.block?.attributes?.payload?.background_image;
+        },
+        backgroundType() {
+            return this.block?.attributes?.payload?.background_type;
+        },
+        complementBackgroundColor() {
+            return this.calcComplement(this.backgroundColor);
+        },
+        icons() {
+            return this.contentIcons;
+        },
+        colors() {
+            return this.mixinColors;
+        },
+        iconColors() {
+            return this.mixinColors.filter(color => color.icon && color.class !== 'studip-lightblue');
+        },
+        textStyle() {
+            let style = {};
+            style.color = this.currentTextColor;
+
+            return style;
+        },
+        headlineStyle() {
+            let style = {};
+            style['background-color'] = this.currentBackgroundColor;
+            if (this.currentBackgroundType === 'image') {
+                style['background-image'] = 'url(' + this.currentBackgroundURL + ')';
+                style['background-color'] = 'transparent';
+            }
+            if (this.currentBackgroundType === 'structural-element-image') {
+                if (this.currentStructuralElementImageURL) {
+                    style['background-image'] = 'url(' + this.currentStructuralElementImageURL + ')';
+                    style['background-color'] = 'transparent';
+                } else {
+                    style['background-color'] = '#28497c';
+                }
+
+            }
+            if (this.hasGradient) {
+                style['background-color'] = 'transparent';
+            }
+
+            return style;
+        },
+        headlineTextboxStyle() {
+            return {
+                rgba: { 'background-color': this.hexToRgbA(this.currentTextBackgroundColor, '0.5') },
+                hex: { 'background-color': this.currentTextBackgroundColor }
+            };
+        },
+        hasSubtitle() {
+            return !['bigicon_before'].includes(this.currentStyle);
+        },
+        subtitleIsSet() {
+            return this.currentSubtitle !== '';
+        },
+        hasSecondSubtitle() {
+            return ['skew_text'].includes(this.currentStyle);
+        },
+        hasIcon() {
+            return ['bigicon_top', 'bigicon_before', 'icon_top_lines'].includes(this.currentStyle);
+        },
+        hasTextBackgroundColor() {
+            return ['ribbon', 'vertical'].includes(this.currentStyle);
+        },
+        hasGradient() {
+            return this.currentBackgroundType === 'gradient';
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            loadFileRef: 'file-refs/loadById',
+            loadStockImage: 'stock-images/loadById',
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentSubtitle = this.subtitle;
+            this.currentSecondSubtitle = this.secondSubtitle;
+            this.currentStyle = this.style;
+            this.currentHeight = this.height;
+            this.currentBackgroundColor = this.backgroundColor;
+            this.currentGradient = this.gradient !== '' ? this.gradient : 'color-blue';
+            this.currentTextColor = this.textColor;
+            this.currentTextBackgroundColor = this.textBackgroundColor;
+            this.currentIcon = this.icon;
+            this.currentIconColor = this.iconColor;
+            this.currentBackgroundType = this.backgroundType;
+            this.currentBackgroundImageId = this.backgroundImageId;
+            this.currentBackgroundImageType = this.backgroundImageType ?? 'file-refs';
+            if (this.currentBackgroundImageId !== '') {
+                this.loadFile();
+            }
+        },
+        async loadFile() {
+            const id = this.currentBackgroundImageId;
+            const type = this.currentBackgroundImageType;
+
+            if (type === 'file-refs') {
+                const options = { include: 'terms-of-use' };
+                await this.loadFileRef({ id: id, options });
+                const fileRef = this.fileRefById({ id: id });
+                if (
+                    fileRef &&
+                    this.relatedTermOfUse({ parent: fileRef, relationship: 'terms-of-use' }).attributes[
+                        'download-condition'
+                    ] === 0
+                ) {
+                    this.updateCurrentBackgroundImage({
+                        id: fileRef.id,
+                        type: 'file-refs',
+                        name: fileRef.attributes.name,
+                        download_url: this.urlHelper.getURL(
+                            'sendfile.php',
+                            { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
+                            true
+                        ),
+                    });
+                }
+            } else if (type === 'stock-images') {
+                await this.loadStockImage({ id });
+                const stockImage = this.stockImageById({ id });
+                if (stockImage) {
+                    this.selectedStockImage = stockImage;
+                    this.updateCurrentBackgroundImage({
+                        id,
+                        type: 'stock-images',
+                        name: stockImage.attributes.title,
+                        download_url:
+                            stockImage.attributes['download-urls']['medium'] ??
+                            stockImage.attributes['download-urls']['small'],
+                    });
+                }
+            }
+        },
+        updateCurrentBackgroundImage(file) {
+            this.currentBackgroundImage = file;
+            this.currentBackgroundImageId = file.id;
+            this.currentBackgroundImageType = file.type;
+            this.currentBackgroundURL = file.download_url;
+        },
+        onSelectFile(file) {
+            this.updateCurrentBackgroundImage({ ...file, type: 'file-refs' });
+        },
+        storeText() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.title = this.currentTitle;
+            attributes.payload.subtitle = this.currentSubtitle;
+            attributes.payload.second_subtitle = this.currentSecondSubtitle;
+            attributes.payload.style = this.currentStyle;
+            attributes.payload.height = this.currentHeight;
+            attributes.payload.text_color = this.currentTextColor;
+            attributes.payload.text_background_color = this.currentTextBackgroundColor;
+            attributes.payload.icon = this.currentIcon;
+            attributes.payload.icon_color = this.currentIconColor;
+            attributes.payload.background_type = this.currentBackgroundType;
+            attributes.payload.background_color = '';
+            attributes.payload.gradient = '';
+            attributes.payload.background_image_id = '';
+            attributes.payload.background_image_type = '';
+
+            if (this.currentBackgroundType === 'color') {
+                attributes.payload.background_color = this.currentBackgroundColor;
+            }
+            if (this.currentBackgroundType === 'image') {
+                attributes.payload.background_image_id = this.currentBackgroundImageId;
+                attributes.payload.background_image_type = this.currentBackgroundImageType;            }
+            if (this.currentBackgroundType === 'gradient') {
+                attributes.payload.gradient = this.currentGradient;
+            }
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+        calcComplement(color) {
+            const RGB = this.calcRGB(color);
+
+            return '#' + this.compToHex(255 - RGB.r) + this.compToHex(255 - RGB.g) + this.compToHex(255 - RGB.b);
+        },
+        calcIconColor(color) {
+            const RGB = this.calcRGB(color);
+
+            return (RGB.r + RGB.g + RGB.b) / 3 > 129 ? 'black' : 'white';
+        },
+        calcRGB(color) {
+            color = color.slice(1); // remove #
+            let val = parseInt(color, 16);
+            let r = val >> 16;
+            let g = (val >> 8) & 0x00ff;
+            let b = val & 0x0000ff;
+
+            if (g > 255) {
+                g = 255;
+            } else if (g < 0) {
+                g = 0;
+            }
+            if (b > 255) {
+                b = 255;
+            } else if (b < 0) {
+                b = 0;
+            }
+
+            return { r: r, g: g, b: b };
+        },
+        compToHex(comp) {
+            let hex = comp.toString(16);
+            return hex.length === 1 ? '0' + hex : hex;
+        },
+        hexToRgbA(hex, a){
+            const RGB = this.calcRGB(hex);
+
+            return 'rgba(' + RGB.r + ',' + RGB.g + ',' + RGB.b + ',' + a +')';
+        },
+        onSelectStockImage(stockImage) {
+            this.updateCurrentBackgroundImage({
+                id: stockImage.id,
+                type: 'stock-images',
+                name: stockImage.attributes.title,
+                download_url:
+                    stockImage.attributes['download-urls']['medium'] ?? stockImage.attributes['download-urls']['small'],
+            });
+            this.selectedStockImage = stockImage;
+            this.showStockImageSelector = false;
+        },
+        onClickRemoveBackgroundImage() {
+            this.currentBackgroundImageId = '';
+            this.currentBackgroundImageType = '';
+            this.selectedStockImage = null;
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+    @import "../../../../assets/stylesheets/scss/courseware/blocks/headline.scss";
+</style>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
new file mode 100644
index 0000000..90df3f6
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
@@ -0,0 +1,258 @@
+<template>
+    <div class="cw-block cw-block-iframe">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
+                <iframe
+                    v-show="currentUrl.includes('http')"
+                    :src="activeUrl"
+                    :height="currentHeight"
+                    width="100%"
+                    allowfullscreen
+                    sandbox="allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts"
+                />
+                <div v-if="currentCcInfo" class="cw-block-iframe-cc-data">
+                    <span class="cw-block-iframe-cc" :class="['cw-block-iframe-cc-' + currentCcInfo]"></span>
+                    <div class="cw-block-iframe-cc-infos">
+                        <p v-if="currentCcWork">{{ $gettext('Werk') }}: {{ currentCcWork }}</p>
+                        <p v-if="currentCcAuthor">{{ $gettext('Autor') }}: {{ currentCcAuthor }}</p>
+                        <p v-if="currentCcBase">{{ $gettext('Lizenz der Plattform') }}: {{ currentCcBase }}</p>
+                    </div>
+                </div>
+                <div v-show="!currentUrl.includes('http')" :style="{ height: currentHeight + 'px' }"></div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <courseware-tabs>
+                    <courseware-tab :index="0" :name="$gettext('Grunddaten')" :selected="true">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Titel') }}
+                                <input type="text" v-model="currentTitle" />
+                            </label>
+                            <label>
+                                {{ $gettext('URL') }}
+                                <input type="text" v-model="currentUrl" @change="setProtocol" />
+                            </label>
+                            <label>
+                                {{ $gettext('Höhe') }}
+                                <input type="number" v-model="currentHeight" min="0" />
+                            </label>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab :index="1" :name="$gettext('Nutzerspezifische ID')">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Nutzerspezifische ID übergeben') }}
+                                <select v-model="currentSubmitUserId">
+                                    <option value="false">{{ $gettext('Nein') }}</option>
+                                    <option value="true">{{ $gettext('Ja') }}</option>
+                                </select>
+                            </label>
+
+                            <label v-if="currentSubmitUserId === 'true'">
+                                {{ $gettext('Name des Übergabeparameters') }}
+                                <input type="text" v-model="currentSubmitParam" />
+                            </label>
+                            <label v-if="currentSubmitUserId === 'true'">
+                                {{ $gettext('Zufallszeichen für Verschlüsselung (Salt)') }}
+                                <input type="text" v-model="currentSalt" />
+                            </label>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab :index="2" :name="$gettext('Creative Commons')">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Creative Commons Lizenz') }}
+                                <select v-model="currentCcInfo">
+                                    <option value="false">{{ $gettext('Keine') }}</option>
+                                    <option value="by">(by) {{ $gettext('Namensnennung') }}</option>
+                                    <option value="by-sa">
+                                        (by-sa) {{ $gettext('Namensnennung & Weitergabe unter gleichen Bedingungen') }}
+                                    </option>
+                                    <option value="by-nc">
+                                        (by-nc) {{ $gettext('Namensnennung & Nicht kommerziell') }}
+                                    </option>
+                                    <option value="by-nd">
+                                        (by-nd) {{ $gettext('Namensnennung & Keine Bearbeitung') }}
+                                    </option>
+                                    <option value="by-nc-nd">
+                                        (by-nc-nd)
+                                        {{ $gettext('Namensnennung & Nicht kommerziell & Keine Bearbeitung') }}
+                                    </option>
+                                    <option value="by-nc-sa">
+                                        (by-nc-sa)
+                                        {{
+                                            $gettext(
+                                                'Namensnennung & Nicht kommerziell & Weitergabe unter gleichen Bedingungen'
+                                            )
+                                        }}
+                                    </option>
+                                </select>
+                            </label>
+                            <label v-if="currentCcInfo !== 'false'">
+                                CC {{ $gettext('Werk') }}
+                                <input type="text" v-model="currentCcWork" />
+                            </label>
+                            <label v-if="currentCcInfo !== 'false'">
+                                CC {{ $gettext('Author') }}
+                                <input type="text" v-model="currentCcAuthor" />
+                            </label>
+                            <label v-if="currentCcInfo !== 'false'">
+                                CC {{ $gettext('Lizenz der Plattform') }}
+                                <input type="text" v-model="currentCcBase" />
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum IFrame-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+import md5 from 'md5';
+
+export default {
+    name: 'courseware-iframe-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentUrl: '',
+            currentHeight: '',
+            currentSubmitUserId: '',
+            currentSubmitParam: '',
+            currentSalt: '',
+            currentCcInfo: '',
+            currentCcWork: '',
+            currentCcAuthor: '',
+            currentCcBase: '',
+        };
+    },
+    computed: {
+        ...mapGetters(['userId']),
+        url() {
+            return this.block?.attributes?.payload?.url;
+        },
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        height() {
+            return this.block?.attributes?.payload?.height;
+        },
+        submitUserId() {
+            return this.block?.attributes?.payload?.submit_user_id;
+        },
+        submitParam() {
+            return this.block?.attributes?.payload?.submit_param;
+        },
+        salt() {
+            return this.block?.attributes?.payload?.salt;
+        },
+        ccInfo() {
+            return this.block?.attributes?.payload?.cc_info;
+        },
+        ccWork() {
+            return this.block?.attributes?.payload?.cc_work;
+        },
+        ccAuthor() {
+            return this.block?.attributes?.payload?.cc_author;
+        },
+        ccBase() {
+            return this.block?.attributes?.payload?.cc_base;
+        },
+        activeUrl() {
+            if (this.currentUrl) {
+                let url = new URL(this.currentUrl);
+                if (this.currentSubmitUserId === 'true') {
+                    url.searchParams.append(this.currentSubmitParam, md5(this.userId + this.currentSalt));
+                }
+
+                return url.href;
+            }
+
+            return '';
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentUrl = this.url;
+            this.currentHeight = this.height;
+            this.currentSubmitUserId = this.submitUserId;
+            this.currentSubmitParam = this.submitParam;
+            this.currentSalt = this.salt;
+            this.currentCcInfo = this.ccInfo;
+            this.currentCcWork = this.ccWork;
+            this.currentCcAuthor = this.ccAuthor;
+            this.currentCcBase = this.ccBase;
+            this.setProtocol();
+        },
+        setProtocol() {
+            if (location.protocol === 'https:') {
+                if (!this.currentUrl.includes('https:')) {
+                    if (this.currentUrl.includes('http:')) {
+                        this.currentUrl = this.currentUrl.replace('http', 'https');
+                    } else {
+                        this.currentUrl = 'https://' + this.currentUrl;
+                    }
+                }
+            } else if (location.protocol === 'http:') {
+                if (!this.currentUrl.includes('http:') && !this.currentUrl.includes('https:')) {
+                    this.currentUrl = 'http://' + this.currentUrl;
+                }
+            }
+        },
+
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.title = this.currentTitle;
+            attributes.payload.url = this.currentUrl;
+            attributes.payload.height = this.currentHeight;
+            attributes.payload.submit_user_id = this.currentSubmitUserId;
+            attributes.payload.submit_param = this.currentSubmitParam;
+            attributes.payload.salt = this.currentSalt;
+            attributes.payload.cc_info = this.currentCcInfo;
+            attributes.payload.cc_work = this.currentCcWork;
+            attributes.payload.cc_author = this.currentCcAuthor;
+            attributes.payload.cc_base = this.currentCcBase;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/iframe.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
new file mode 100644
index 0000000..41b39d2
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
@@ -0,0 +1,804 @@
+<template>
+    <div class="cw-block cw-block-image-map" @mousedown="selectShape">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <img :src="currentUrl" class="cw-image-map-original-img" ref="original_img" @load="buildCanvas" />
+                <canvas class="cw-image-map-canvas" ref="canvas"></canvas>
+                <img
+                    class="cw-image-from-canvas"
+                    :src="image_from_canvas"
+                    ref="image_from_canvas"
+                    :usemap="'#' + map_name"
+                />
+                <map ref="map" :name="map_name">
+                    <area
+                        v-for="area in areas"
+                        :key="area.id"
+                        :id="area.id"
+                        :shape="area.shape"
+                        :coords="area.coords"
+                        :title="area.title"
+                        :href="area.external_target"
+                        :target="area.link_target"
+                        @click="
+                            if (area.target_type === 'internal') {
+                                areaLink(area.internal_target);
+                            }
+                        "
+                    />
+                </map>
+                <div
+                    v-if="showEditMode && viewMode === 'edit' && currentShapes.length > 0"
+                    ref="draggableShapeWrapper"
+                    class="cw-draggable-shapes-wrapper"
+                >
+                    <vue-resizeable
+                        v-for="(shape, index) in currentShapes"
+                        :key="index"
+                        :index="index"
+                        style="position: absolute"
+                        ref="resizableAreaComponents"
+                        :fitParent="true"
+                        :dragSelector="dragSelector"
+                        :active="handlers"
+                        :left="getShapeOffsetLeft(shape)"
+                        :top="getShapeOffsetTop(shape)"
+                        :width="getShapeWidth(shape)"
+                        :height="getShapeHeight(shape)"
+                        @resize:start="dragStartHandler"
+                        @resize:end="endDraggingShape"
+                        @drag:start="dragStartHandler"
+                        @drag:end="endDraggingShape"
+                    >
+                        <div
+                            class="cw-draggable-area"
+                            :style="{
+                                backgroundColor: getColorRGBA(shape.data.color),
+                                color: shape.data.textcolor ? getColorRGBA(shape.data.textcolor) : '',
+                                borderRadius: getShapeBorderRadius(shape),
+                                border: getShapeBorder(shape),
+                                cursor: selectedShapeIndex !== false ? 'grabbing' : '',
+                            }"
+                            @click="followLink(index)"
+                        >
+                            {{ shape.data.text }}
+                        </div>
+                    </vue-resizeable>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Bilddatei') }}
+                        <courseware-file-chooser
+                            v-model="currentFileId"
+                            :isImage="true"
+                            @selectFile="updateCurrentFile"
+                        />
+                    </label>
+                    <label>
+                        <a class="button add" @click="addShape('arc')">{{ $gettext('Kreis hinzufügen') }}</a>
+                        <a class="button add" @click="addShape('ellipse')">{{ $gettext('Oval hinzufügen') }}</a>
+                        <a class="button add" @click="addShape('rect')">{{ $gettext('Rechteck hinzufügen') }}</a>
+                    </label>
+                    <courseware-tabs v-if="currentShapes.length > 0">
+                        <courseware-tab
+                            v-for="(shape, index) in currentShapes"
+                            :key="index"
+                            :index="index"
+                            :name="shape.title ? shape.title : ''"
+                            :icon="shape.title === '' ? 'link-extern' : ''"
+                            :selected="index === 0"
+                        >
+                            <label class="col-2">
+                                {{ $gettext('Farbe') }}
+                                <studip-select
+                                    :options="colors"
+                                    label="name"
+                                    :reduce="(color) => color.class"
+                                    :clearable="false"
+                                    v-model="shape.data.color"
+                                    @input="drawScreen"
+                                >
+                                    <template #open-indicator="selectAttributes">
+                                        <span v-bind="selectAttributes"
+                                            ><studip-icon shape="arr_1down" size="10"
+                                        /></span>
+                                    </template>
+                                    <template #no-options>
+                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                                    </template>
+                                    <template #selected-option="{ name, rgba }">
+                                        <span class="vs__option-color" :style="{ 'background-color': rgba }"></span
+                                        ><span>{{ name }}</span>
+                                    </template>
+                                    <template #option="{ name, rgba }">
+                                        <span class="vs__option-color" :style="{ 'background-color': rgba }"></span
+                                        ><span>{{ name }}</span>
+                                    </template>
+                                </studip-select>
+                            </label>
+                            <label class="col-2">
+                                {{ $gettext('Textfarbe') }}
+                                <studip-select
+                                    :options="colors"
+                                    label="name"
+                                    :reduce="(color) => color.class"
+                                    :clearable="false"
+                                    v-model="shape.data.textcolor"
+                                    @input="drawScreen"
+                                >
+                                    <template #open-indicator="selectAttributes">
+                                        <span v-bind="selectAttributes"
+                                            ><studip-icon shape="arr_1down" size="10"
+                                        /></span>
+                                    </template>
+                                    <template #no-options>
+                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                                    </template>
+                                    <template #selected-option="{ name, rgba }">
+                                        <span class="vs__option-color" :style="{ 'background-color': rgba }"></span
+                                        ><span>{{ name }}</span>
+                                    </template>
+                                    <template #option="{ name, rgba }">
+                                        <span class="vs__option-color" :style="{ 'background-color': rgba }"></span
+                                        ><span>{{ name }}</span>
+                                    </template>
+                                </studip-select>
+                            </label>
+                            <br />
+                            <template v-if="shape.type === 'arc'">
+                                <label class="col-1">
+                                    X
+                                    <input type="number" v-model="shape.data.centerX" @change="drawScreen" />
+                                </label>
+                                <label class="col-1">
+                                    Y
+                                    <input type="number" v-model="shape.data.centerY" @change="drawScreen" />
+                                </label>
+                                <label class="col-1">
+                                    {{ $gettext('Radius') }}
+                                    <input type="number" v-model="shape.data.radius" @change="drawScreen" />
+                                </label>
+                            </template>
+                            <template v-if="shape.type === 'rect'">
+                                <label class="col-1">
+                                    X
+                                    <input type="number" v-model="shape.data.X" @change="drawScreen" />
+                                </label>
+                                <label class="col-1">
+                                    Y
+                                    <input type="number" v-model="shape.data.Y" @change="drawScreen" />
+                                </label>
+                                <label class="col-1">
+                                    {{ $gettext('Höhe') }}
+                                    <input type="number" v-model="shape.data.height" @change="drawScreen" />
+                                </label>
+                                <label class="col-1">
+                                    {{ $gettext('Breite') }}
+                                    <input type="number" v-model="shape.data.width" @change="drawScreen" />
+                                </label>
+                            </template>
+                            <template v-if="shape.type === 'ellipse'">
+                                <label class="col-1">
+                                    X
+                                    <input type="number" v-model="shape.data.X" @change="drawScreen" />
+                                </label>
+                                <label class="col-1">
+                                    Y
+                                    <input type="number" v-model="shape.data.Y" @change="drawScreen" />
+                                </label>
+                                <label class="col-1">
+                                    {{ $gettext('Radius') }} X
+                                    <input type="number" v-model="shape.data.radiusX" @change="drawScreen" />
+                                </label>
+                                <label class="col-1">
+                                    {{ $gettext('Radius') }} Y
+                                    <input type="number" v-model="shape.data.radiusY" @change="drawScreen" />
+                                </label>
+                            </template>
+                            <label class="col-4">
+                                {{ $gettext('Bezeichnung') }}
+                                <input type="text" v-model="shape.title" />
+                            </label>
+                            <label class="col-4">
+                                {{ $gettext('Beschriftung') }}
+                                <input type="text" v-model="shape.data.text" @change="drawScreen" />
+                            </label>
+                            <br />
+                            <label class="col-2">
+                                {{ $gettext('Art des Links') }}
+                                <select v-model="shape.link_type">
+                                    <option value="internal">{{ $gettext('Interner Link') }}</option>
+                                    <option value="external">{{ $gettext('Externer Link') }}</option>
+                                </select>
+                            </label>
+                            <label v-if="shape.link_type === 'internal'" class="col-2">
+                                {{ $gettext('Ziel des Links') }}
+                                <select v-model="shape.target_internal" @change="drawScreen">
+                                    <option v-for="(el, index) in courseware" :key="index" :value="el.id">
+                                        {{ el.attributes.title }}
+                                    </option>
+                                </select>
+                            </label>
+                            <label v-if="shape.link_type === 'external'" class="col-2">
+                                {{ $gettext('Ziel des Links') }}
+                                <input
+                                    type="text"
+                                    placeholder="https://www.studip.de"
+                                    v-model="shape.target_external"
+                                    @change="
+                                        drawScreen();
+                                        fixUrl(index);
+                                    "
+                                />
+                            </label>
+                            <label>
+                                <a class="button cancel" @click="removeShape(index)">{{
+                                    $gettext('Form entfernen')
+                                }}</a>
+                            </label>
+                        </courseware-tab>
+                    </courseware-tabs>
+                </form>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Verweissensitive-Grafik-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import VueResizeable from 'vrp-vue-resizable';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-image-map-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, { VueResizeable }),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentFileId: '',
+            currentFile: {},
+            currentShapes: {},
+            context: {},
+            image_from_canvas: '',
+            map_name: '',
+            areas: [],
+            darkColors: ['black', 'darkgrey', 'purple'],
+            colors: [
+                { name: this.$gettext('Transparent'), class: 'transparent', rgba: 'rgba(0,0,0,0)' },
+                { name: this.$gettext('Weiß'), class: 'white', rgba: 'rgba(255,255,255,1)' },
+                { name: this.$gettext('Blau'), class: 'blue', rgba: 'rgba(52,152,219,1)' },
+                { name: this.$gettext('Grün'), class: 'green', rgba: 'rgba(46,204,113,1)' },
+                { name: this.$gettext('Lila'), class: 'purple', rgba: 'rgba(155,89,182,1)' },
+                { name: this.$gettext('Rot'), class: 'red', rgba: 'rgba(231,76,60,1)' },
+                { name: this.$gettext('Gelb'), class: 'yellow', rgba: 'rgba(254,211,48,1)' },
+                { name: this.$gettext('Orange'), class: 'orange', rgba: 'rgba(243,156,18,1)' },
+                { name: this.$gettext('Grau'), class: 'grey', rgba: 'rgba(236, 240, 241,1)' },
+                { name: this.$gettext('Dunkelgrau'), class: 'darkgrey', rgba: 'rgba(52,73,94,1)' },
+                { name: this.$gettext('Schwarz'), class: 'black', rgba: 'rgba(0,0,0,1)' },
+            ],
+            file: null,
+            dragSelector: '.cw-draggable-area',
+            handlers: ['r', 'rb', 'b', 'lb', 'l', 'lt', 't', 'rt'],
+            draggedShapeWidth: 50,
+            draggedShapeHeight: 50,
+            selectedShapeIndex: false,
+            draggingActive: false,
+            showEditMode: false,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            courseware: 'courseware-structural-elements/all',
+            fileRefById: 'file-refs/byId',
+            urlHelper: 'urlHelper',
+            viewMode: 'viewMode',
+        }),
+        fileId() {
+            return this.block?.attributes?.payload?.file_id;
+        },
+        shapes() {
+            return this.block?.attributes?.payload?.shapes;
+        },
+        currentUrl() {
+            if (this.currentFile.download_url !== 'undefined') {
+                return this.currentFile.download_url;
+            } else {
+                return '';
+            }
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            loadFileRef: 'file-refs/loadById',
+        }),
+        async initCurrentData(event) {
+            this.showEditMode = Boolean(event);
+            this.currentFileId = this.fileId;
+            this.currentShapes = JSON.parse(JSON.stringify(this.shapes));
+            await this.loadFile();
+            this.buildCanvas();
+        },
+        async loadFile() {
+            const id = this.currentFileId;
+            await this.loadFileRef({ id });
+            const fileRef = this.fileRefById({ id });
+
+            if (fileRef) {
+                this.updateCurrentFile({
+                    id: fileRef.id,
+                    name: fileRef.attributes.name,
+                    download_url: this.urlHelper.getURL(
+                        'sendfile.php',
+                        { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
+                        true
+                    ),
+                });
+            }
+        },
+        updateCurrentFile(file) {
+            this.currentFile = file;
+            this.currentFileId = file.id;
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.file_id = this.currentFileId;
+            attributes.payload.shapes = this.currentShapes;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+
+        buildCanvas() {
+            let canvas = this.$refs.canvas;
+            let original_img = this.$refs.original_img;
+            canvas.width = 1085;
+            if (original_img.height > 0) {
+                canvas.height = Math.round((canvas.width / original_img.width) * original_img.height);
+            } else {
+                canvas.height = 484;
+            }
+            this.context = canvas.getContext('2d');
+            this.drawScreen();
+        },
+        drawScreen() {
+            let context = this.context;
+            let view = this;
+            let outlineImage = new Image();
+            outlineImage.src = this.currentUrl;
+            outlineImage.onload = function () {
+                context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
+                context.fillStyle = '#ffffff';
+                context.fillRect(0, 0, context.canvas.width, context.canvas.height); // set background
+                if (outlineImage.src !== '') {
+                    context.drawImage(outlineImage, 0, 0, context.canvas.width, context.canvas.height);
+                }
+                view.drawShapes();
+
+                if (!(view.$refs.canvas.length > 0)) {
+                    view.image_from_canvas = view.context.canvas.toDataURL('image/jpeg', 1.0);
+                    view.mapImage();
+                }
+            };
+        },
+        drawShapes() {
+            let context = this.context;
+            let view = this;
+            this.currentShapes.forEach((value, index) => {
+                // skip the selected shape when redrawing so it disappears while dragging the shape
+                if (this.selectedShapeIndex !== index) {
+                    let shape = value;
+                    let text = shape.data.text;
+                    let shape_width = 0;
+                    let shape_height = 0;
+                    let text_X = 0;
+                    let text_Y = 0;
+
+                    context.beginPath();
+                    switch (shape.type) {
+                        case 'arc':
+                            shape_width = Math.round((2 * shape.data.radius) / Math.sqrt(2)) * 0.85;
+                            shape_height = shape_width / 0.85;
+                            text_X = shape.data.centerX;
+                            text_Y = shape.data.centerY - shape.data.radius * 0.75;
+                            context.arc(shape.data.centerX, shape.data.centerY, shape.data.radius, 0, 2 * Math.PI); // x, y, r, startAngle, endAngle ... Angle in radians!
+                            context.fillStyle = view.colors.filter((color) => {
+                                return color.class === shape.data.color;
+                            })[0].rgba;
+                            context.fill();
+                            break;
+                        case 'ellipse':
+                            shape_width = shape.data.radiusX;
+                            shape_height = shape.data.radiusY * 1.75;
+                            text_X = shape.data.X;
+                            text_Y = shape.data.Y - shape.data.radiusY * 0.8;
+                            context.ellipse(
+                                shape.data.X,
+                                shape.data.Y,
+                                shape.data.radiusX,
+                                shape.data.radiusY,
+                                0,
+                                0,
+                                2 * Math.PI
+                            );
+                            context.fillStyle = view.colors.filter((color) => {
+                                return color.class === shape.data.color;
+                            })[0].rgba;
+                            context.fill();
+                            break;
+                        case 'rect':
+                            shape_width = shape.data.width;
+                            shape_height = shape.data.height;
+                            text_X = shape.data.X + shape.data.width / 2;
+                            text_Y = shape.data.Y;
+                            context.rect(shape.data.X, shape.data.Y, shape.data.width, shape.data.height);
+                            context.fillStyle = view.colors.filter((color) => {
+                                return color.class === shape.data.color;
+                            })[0].rgba;
+                            context.fill();
+                            break;
+                        default:
+                            return;
+                    }
+
+                    if (text && shape.data.color !== 'transparent') {
+                        text = view.fitTextToShape(context, text, shape_width);
+                        context.textAlign = 'center';
+                        context.font = '14px Arial';
+                        if (shape.data.textcolor) {
+                            context.fillStyle = this.getColorRGBA(shape.data.textcolor);
+                        } else {
+                            if (view.darkColors.indexOf(shape.data.color) > -1) {
+                                context.fillStyle = '#ffffff';
+                            } else {
+                                context.fillStyle = '#000000';
+                            }
+                        }
+                        let lineHeight = shape_height / (text.length + 1);
+                        text.forEach((value, key) => {
+                            context.fillText(value, text_X, text_Y + lineHeight * (key + 1));
+                        });
+                    }
+
+                    context.closePath();
+                }
+            });
+        },
+        fitTextToShape(context, text, shapeWidth) {
+            shapeWidth = shapeWidth || 0;
+
+            let newText = [];
+
+            if (shapeWidth <= 0) {
+                return [text];
+            }
+            let words = text.split(' ');
+            let i = 1;
+            while (words.length > 0 && i <= words.length) {
+                let word = words.slice(0, i).join(' ');
+                let wordWidth = context.measureText(word).width + 2;
+                if (wordWidth > shapeWidth) {
+                    if (i === 1) {
+                        i = 2;
+                    }
+                    newText.push(words.slice(0, i - 1).join(' '));
+                    words = words.splice(i - 1);
+                    i = 1;
+                } else {
+                    i++;
+                }
+            }
+            if (i > 0) {
+                newText.push(words.join(' '));
+            }
+
+            return newText;
+        },
+        mapImage() {
+            let view = this;
+            // generate map name
+            let map_name = 'cw-image-map-' + Math.round(Math.random() * 100);
+            this.map_name = map_name;
+
+            // insert areas
+            this.areas = [];
+            this.currentShapes.forEach((value, key) => {
+                let shape = value;
+                let area = {};
+                let coords = '';
+                let x = 0;
+                let y = 0;
+                area.id = 'shape-' + key;
+
+                switch (shape.type) {
+                    case 'arc':
+                        area.shape = 'circle';
+                        area.coords = shape.data.centerX + ', ' + shape.data.centerY + ', ' + shape.data.radius;
+                        break;
+                    case 'ellipse':
+                        for (let theta = 0; theta < 2 * Math.PI; theta += (2 * Math.PI) / 20) {
+                            x = parseInt(shape.data.X) + Math.round(parseInt(shape.data.radiusX) * Math.cos(theta));
+                            y = parseInt(shape.data.Y) + Math.round(parseInt(shape.data.radiusY) * Math.sin(theta));
+                            coords = coords + x + ',' + y + ',';
+                        }
+                        area.shape = 'poly';
+                        area.coords = coords;
+                        break;
+                    case 'rect':
+                    case 'text':
+                        x = parseInt(shape.data.X) + parseInt(shape.data.width);
+                        y = parseInt(shape.data.Y) + parseInt(shape.data.height);
+                        area.shape = 'rect';
+                        area.coords = shape.data.X + ', ' + shape.data.Y + ', ' + x + ', ' + y;
+                        break;
+                }
+                area.title = shape.title;
+                shape.link_type === 'external'
+                    ? (area.external_target = shape.target_external)
+                    : (area.external_target = '#');
+                if (shape.link_type === 'internal') {
+                    area.internal_target = shape.target_internal;
+                } else {
+                    area.internal_target = '';
+                }
+                shape.link_type === 'external' ? (area.link_target = '_blank') : (area.link_target = '_self');
+                area.link_type = shape.link_type;
+                area.target_type = shape.link_type;
+                view.areas.push(area);
+            });
+        },
+        areaLink(target) {
+            this.$router.push(target);
+        },
+
+        //edit methods
+        addShape(addtype) {
+            let data = {};
+            switch (addtype) {
+                case 'arc':
+                    data = {
+                        centerX: 50,
+                        centerY: 50,
+                        radius: 50,
+                        color: 'blue',
+                        border: false,
+                        text: '',
+                    };
+                    break;
+                case 'rect':
+                    data = {
+                        X: 50,
+                        Y: 50,
+                        height: 100,
+                        width: 50,
+                        color: 'blue',
+                        border: false,
+                        text: '',
+                    };
+                    break;
+                case 'ellipse':
+                    data = {
+                        X: 50,
+                        Y: 50,
+                        radiusX: 50,
+                        radiusY: 20,
+                        color: 'blue',
+                        border: false,
+                        text: '',
+                    };
+                    break;
+            }
+            this.currentShapes.push({
+                type: addtype,
+                data: data,
+                title: '',
+                link_type: 'external',
+                target_internal: '',
+                target_external: '',
+            });
+            this.buildCanvas();
+        },
+        removeShape(index) {
+            this.currentShapes.splice(index, 1);
+            this.drawScreen();
+        },
+        fixUrl(index) {
+            let url = this.currentShapes[index].target_external;
+            if (url !== '' && url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
+                url = 'https://' + url;
+            }
+            this.currentShapes[index].target_external = url;
+        },
+        dragStartHandler(data) {
+            // redraw screen now that a shape was selected so that it disappears while dragging or resizing
+            this.drawScreen();
+        },
+        selectShape(data) {
+            // set current draggable div shape to canvas shape coordinates
+            let canvas = this.$refs.image_from_canvas;
+            let canvasSpecs = canvas.getBoundingClientRect();
+            let mouseX = (data.clientX - canvasSpecs.left) * (canvas.width / canvasSpecs.width);
+            let mouseY = (data.clientY - canvasSpecs.top) * (canvas.height / canvasSpecs.height);
+            this.currentShapes.forEach((value, key) => {
+                let shape = value;
+                // if the event target is the draggable area, check for the shape area normally
+                // else check if the click was on a resizable area that belongs to a shape since
+                // resizable areas are partly outside the shape
+                if (data.target.classList.contains('cw-draggable-area')) {
+                    if (this.mouseHit(mouseX, mouseY, shape)) {
+                        this.selectedShapeIndex = key;
+                    }
+                } else {
+                    mouseX = data.target.parentElement.offsetLeft;
+                    mouseY = data.target.parentElement.offsetTop;
+                    if (shape.type == 'arc') {
+                        mouseX += shape.data.radius;
+                        mouseY += shape.data.radius;
+                    }
+                    if (shape.type == 'rect') {
+                        mouseX += shape.data.width / 2;
+                        mouseY += shape.data.height / 2;
+                    }
+                    if (shape.type == 'ellipse') {
+                        mouseX += shape.data.radiusX;
+                        mouseY += shape.data.radiusY;
+                    }
+                    if (this.mouseHit(mouseX, mouseY, shape)) {
+                        this.selectedShapeIndex = key;
+                    }
+                }
+            });
+        },
+        endDraggingShape(data) {
+            this.draggingActive = true;
+            // transfer div shape data to canvas according to shape
+            let shape = this.currentShapes[this.selectedShapeIndex];
+            if (shape.type == 'arc') {
+                let circle_width = data.width != shape.data.radius * 2 ? data.width : data.height;
+                // if the shape was clicked and not dragged, set the dragging status to false to follow the link
+                if (
+                    shape.data.centerX == data.left + shape.data.radius ||
+                    shape.data.centerY == data.top + shape.data.radius
+                ) {
+                    this.draggingActive = false;
+                }
+                shape.data.radius = circle_width / 2;
+                shape.data.centerX = data.left + shape.data.radius;
+                shape.data.centerY = data.top + shape.data.radius;
+            }
+            if (shape.type == 'rect') {
+                if (shape.data.X == data.left || shape.data.Y == data.top) {
+                    this.draggingActive = false;
+                }
+                shape.data.X = data.left;
+                shape.data.Y = data.top;
+                shape.data.width = data.width;
+                shape.data.height = data.height;
+            }
+            if (shape.type == 'ellipse') {
+                if (shape.data.X == data.left + shape.data.radiusX || shape.data.Y == data.top + shape.data.radiusY) {
+                    this.draggingActive = false;
+                }
+                shape.data.radiusX = data.width / 2;
+                shape.data.radiusY = data.height / 2;
+                shape.data.X = data.left + shape.data.radiusX;
+                shape.data.Y = data.top + shape.data.radiusY;
+            }
+            // unselect shape to stop skipping the selected shape when drawing the canvas
+            this.selectedShapeIndex = false;
+            this.drawScreen();
+        },
+        mouseHit(mouseX, mouseY, shape) {
+            // check if the mouseclick was on a shape and return true if it was
+            if (shape.type == 'arc') {
+                let dx = shape.data.centerX - mouseX;
+                let dy = shape.data.centerY - mouseY;
+                return dx * dx + dy * dy < shape.data.radius * shape.data.radius;
+            }
+            if (shape.type == 'rect' || shape.type == 'text') {
+                let dx = mouseX - shape.data.X;
+                let dy = mouseY - shape.data.Y;
+                return dx <= shape.data.width && dy <= shape.data.height && dx >= 0 && dy >= 0;
+            }
+            if (shape.type == 'ellipse') {
+                let dx = shape.data.X - mouseX;
+                let dy = shape.data.Y - mouseY;
+                return Math.abs(dx) < shape.data.radiusX && Math.abs(dy) < shape.data.radiusY;
+            }
+        },
+        getColorRGBA(color) {
+            return this.colors.filter((col) => {
+                return col.class === color;
+            })[0].rgba;
+        },
+        getShapeBorder(shape) {
+            return shape.data.color === 'transparent' ? 'dashed thin #000' : 'none';
+        },
+        getShapeBorderRadius(shape) {
+            if (shape.type == 'rect') {
+                return 0;
+            } else {
+                return '50%';
+            }
+        },
+        getShapeOffsetLeft(shape) {
+            if (shape.type == 'arc') {
+                return parseInt(shape.data.centerX - shape.data.radius);
+            }
+            if (shape.type == 'rect') {
+                return parseInt(shape.data.X);
+            }
+            if (shape.type == 'ellipse') {
+                return parseInt(shape.data.X) - shape.data.radiusX;
+            }
+        },
+        getShapeOffsetTop(shape) {
+            if (shape.type == 'arc') {
+                return parseInt(shape.data.centerY - shape.data.radius);
+            }
+            if (shape.type == 'rect') {
+                return parseInt(shape.data.Y);
+            }
+            if (shape.type == 'ellipse') {
+                return parseInt(shape.data.Y) - shape.data.radiusY;
+            }
+        },
+        getShapeWidth(shape) {
+            if (shape.type == 'arc') {
+                return parseInt(shape.data.radius * 2);
+            }
+            if (shape.type == 'rect') {
+                return parseInt(shape.data.width);
+            }
+            if (shape.type == 'ellipse') {
+                return parseInt(shape.data.radiusX * 2);
+            }
+        },
+        getShapeHeight(shape) {
+            if (shape.type == 'arc') {
+                return parseInt(shape.data.radius * 2);
+            }
+            if (shape.type == 'rect') {
+                return parseInt(shape.data.height);
+            }
+            if (shape.type == 'ellipse') {
+                return parseInt(shape.data.radiusY * 2);
+            }
+        },
+        followLink(index) {
+            if (!this.draggingActive) {
+                this.$refs.map.areas[index].click();
+            }
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/image-map.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue b/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
new file mode 100644
index 0000000..ab79edc
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
@@ -0,0 +1,184 @@
+<template>
+    <div class="cw-block cw-block-keypoint">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div class="cw-keypoint-content" :class="['cw-keypoint-' + currentColor]">
+                    <studip-icon v-if="currentIcon" size="48" :shape="currentIcon" :role="currentRole" />
+                    <p class="cw-keypoint-sentence">{{ currentText }}</p>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label class="col-4">
+                        {{ $gettext('Merksatz') }}
+                        <input
+                            type="text"
+                            name="cw-keypoint-content"
+                            class="cw-keypoint-set-content"
+                            v-model="currentText"
+                            spellcheck="true"
+                        />
+                    </label>
+                    <br />
+                    <label class="col-2">
+                        {{ $gettext('Farbe') }}
+                        <studip-select
+                            :options="colors"
+                            label="icon"
+                            :clearable="false"
+                            :reduce="(option) => option.icon"
+                            v-model="currentColor"
+                        >
+                            <template #open-indicator="selectAttributes">
+                                <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span>
+                            </template>
+                            <template #no-options>
+                                {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                            </template>
+                            <template #selected-option="{ name, hex }">
+                                <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                                ><span>{{ name }}</span>
+                            </template>
+                            <template #option="{ name, hex }">
+                                <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                                ><span>{{ name }}</span>
+                            </template>
+                        </studip-select>
+                    </label>
+                    <label class="col-2">
+                        {{ $gettext('Icon') }}
+                        <studip-select :options="icons" :clearable="false" v-model="currentIcon">
+                            <template #open-indicator="selectAttributes">
+                                <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span>
+                            </template>
+                            <template #no-options>
+                                {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                            </template>
+                            <template #selected-option="option">
+                                <studip-icon :shape="option.label" />
+                                <span class="vs__option-with-icon">{{ option.label }}</span>
+                            </template>
+                            <template #option="option">
+                                <studip-icon :shape="option.label" />
+                                <span class="vs__option-with-icon">{{ option.label }}</span>
+                            </template>
+                        </studip-select>
+                    </label>
+                </form>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Merksatz-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import contentIconsMixin from '@/vue/mixins/courseware/content-icons.js';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-key-point-block',
+    mixins: [blockMixin, colorMixin, contentIconsMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentText: '',
+            currentColor: '',
+            currentIcon: '',
+        };
+    },
+    computed: {
+        file() {
+            return `icons/${this.color}/${this.icon}.svg`;
+        },
+        icons() {
+            return this.contentIcons;
+        },
+        colors() {
+            return this.mixinColors.filter(
+                (color) => color.icon && color.class !== 'white' && color.class !== 'studip-lightblue'
+            );
+        },
+        text() {
+            return this.block?.attributes?.payload?.text;
+        },
+        color() {
+            return this.block?.attributes?.payload?.color;
+        },
+        icon() {
+            return this.block?.attributes?.payload?.icon;
+        },
+        currentRole() {
+            switch (this.currentColor) {
+                case 'black':
+                    return 'info';
+
+                case 'grey':
+                    return 'inactive';
+
+                case 'green':
+                    return 'status-green';
+
+                case 'red':
+                    return 'status-red';
+
+                case 'white':
+                    return 'info_alt';
+
+                case 'yellow':
+                    return 'status-yellow';
+
+                case 'blue':
+                default:
+                    return 'clickable';
+            }
+        },
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            this.currentText = this.text;
+            this.currentColor = this.color;
+            this.currentIcon = this.icon;
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.text = this.currentText;
+            attributes.payload.color = this.currentColor;
+            attributes.payload.icon = this.currentIcon;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/keypoint.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareLinkBlock.vue b/resources/vue/components/courseware/blocks/CoursewareLinkBlock.vue
new file mode 100644
index 0000000..532c97b
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareLinkBlock.vue
@@ -0,0 +1,152 @@
+<template>
+    <div class="cw-block cw-block-link">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentType === 'external'">
+                    <a :href="currentUrl" target="_blank">
+                        <div class="cw-link external">
+                            <span class="cw-link-title">{{ currentTitle }}</span>
+                        </div>
+                    </a>
+                </div>
+                <div v-if="currentType === 'internal'">
+                    <router-link :to="{ name: 'CoursewareStructuralElement', params: { id: currentTarget } }">
+                        <div class="cw-link internal">
+                            <span class="cw-link-title">
+                                {{ currentTitle }}
+                            </span>
+                        </div>
+                    </router-link>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Titel') }}
+                        <input type="text" v-model="currentTitle" />
+                    </label>
+                    <label>
+                        {{ $gettext('Art des Links') }}
+                        <select v-model="currentType">
+                            <option value="external">{{ $gettext('Extern') }}</option>
+                            <option value="internal">{{ $gettext('Intern') }}</option>
+                        </select>
+                    </label>
+                    <label v-show="currentType === 'external'">
+                        {{ $gettext('URL') }}
+                        <input type="text" v-model="currentUrl" @change="fixUrl" />
+                    </label>
+                    <label v-show="currentType === 'internal'">
+                        {{ $gettext('Seite') }}
+                        <select v-model="currentTarget">
+                            <option v-for="(el, index) in courseware" :key="index" :value="el.id">
+                                {{ el.attributes.title }}
+                            </option>
+                        </select>
+                    </label>
+                </form>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Link-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-link-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentType: '',
+            currentTarget: '',
+            currentUrl: '',
+            currentTitle: '',
+        };
+    },
+    computed: {
+        ...mapGetters({
+            courseware: 'courseware-structural-elements/all',
+        }),
+        type() {
+            return this.block?.attributes?.payload?.type;
+        },
+        target() {
+            return this.block?.attributes?.payload?.target;
+        },
+        url() {
+            return this.block?.attributes?.payload?.url;
+        },
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            companionWarning: 'companionWarning',
+        }),
+        initCurrentData() {
+            this.currentType = this.type;
+            this.currentTarget = this.target;
+            this.currentUrl = this.url;
+            this.currentTitle = this.title;
+            this.fixUrl();
+        },
+        fixUrl() {
+            if (
+                this.currentUrl.indexOf('http://') !== 0 &&
+                this.currentUrl.indexOf('https://') !== 0 &&
+                this.currentUrl !== ''
+            ) {
+                this.currentUrl = 'https://' + this.currentUrl;
+            }
+        },
+        storeBlock() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.type = this.currentType;
+            attributes.payload.target = this.currentTarget;
+            attributes.payload.url = this.currentUrl;
+            attributes.payload.title = this.currentTitle;
+            if (this.currentType === 'internal' && this.currentTarget === '') {
+                this.companionWarning({
+                    info: this.$gettext('Bitte wählen Sie eine Seite als Ziel aus.'),
+                });
+                return false;
+            } else {
+                this.updateBlock({
+                    attributes: attributes,
+                    blockId: this.block.id,
+                    containerId: this.block.relationships.container.data.id,
+                });
+            }
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/link.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareLtiBlock.vue b/resources/vue/components/courseware/blocks/CoursewareLtiBlock.vue
new file mode 100644
index 0000000..626a1a0
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareLtiBlock.vue
@@ -0,0 +1,300 @@
+<template>
+    <div class="cw-block cw-block-lti">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="false"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
+                <iframe
+                    v-if="toolId !== ''"
+                    class="cw-block-lti-content"
+                    :src="iframeUrl"
+                    :height="currentHeight"
+                    width="100%"
+                    allowfullscreen
+                    sandbox="allow-downloads allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts"
+                />
+                <div v-else class="cw-block-lti-content">
+                    <span class="cw-block-lti-icon-tool">
+                        {{ $gettext('Kein LTI-Tool konfiguriert') }}
+                    </span>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <courseware-tabs>
+                    <courseware-tab :index="0" :name="$gettext('Grunddaten')" :selected="true">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Titel') }}
+                                <input type="text" v-model="currentTitle" />
+                            </label>
+                            <label>
+                                {{ $gettext('Auswahl des externen Tools') }}
+                                <select v-model="currentToolId">
+                                    <option v-for="tool in tools" :key="tool.id" :value="tool.id">
+                                        {{ tool.name }}
+                                    </option>
+                                    <option value="0">{{ $gettext('Zugangsdaten selbst eingeben...') }}</option>
+                                </select>
+                            </label>
+                            <label v-show="allowCustomUrl">
+                                {{ $gettext('URL der Anwendung (optional)') }}
+                                <studip-tooltip-icon
+                                    :text="$gettext('Sie können direkt auf eine URL in der Anwendung verlinken.')"
+                                />
+                                <input type="text" v-model="currentLaunchUrl" :placeholder="currentTool?.launch_url" />
+                            </label>
+
+                            <div v-show="customToolSelected">
+                                <label class="studiprequired">
+                                    {{ $gettext('URL der Anwendung') }}
+                                    <span
+                                        class="asterisk"
+                                        :title="$gettext('Dies ist ein Pflichtfeld')"
+                                        aria-hidden="true"
+                                        >*</span
+                                    >
+                                    <studip-tooltip-icon
+                                        :text="
+                                            $gettext(
+                                                'Die Betreiber dieses Tools müssen Ihnen eine URL und Zugangsdaten (Consumer-Key und Consumer-Secret) mitteilen.'
+                                            )
+                                        "
+                                    />
+                                    <input type="text" v-model="currentLaunchUrl" required />
+                                </label>
+                                <label class="studiprequired">
+                                    {{ $gettext('Consumer-Key des LTI-Tools') }}
+                                    <span
+                                        class="asterisk"
+                                        :title="$gettext('Dies ist ein Pflichtfeld')"
+                                        aria-hidden="true"
+                                        >*</span
+                                    >
+                                    <input type="text" v-model="currentConsumerKey" required />
+                                </label>
+                                <label class="studiprequired">
+                                    {{ $gettext('Consumer-Secret des LTI-Tools') }}
+                                    <span
+                                        class="asterisk"
+                                        :title="$gettext('Dies ist ein Pflichtfeld')"
+                                        aria-hidden="true"
+                                        >*</span
+                                    >
+                                    <input type="text" v-model="currentConsumerSecret" required />
+                                </label>
+                                <label>
+                                    {{ $gettext('OAuth Signatur Methode des LTI-Tools') }}
+                                    <select v-model="currentOauthSignatureMethod">
+                                        <option value="sha1">HMAC-SHA1</option>
+                                        <option value="sha256">HMAC-SHA256</option>
+                                    </select>
+                                </label>
+                                <label>
+                                    <input type="checkbox" v-model="currentSendLisPerson" />
+                                    {{ $gettext('Nutzerdaten an LTI-Tool senden') }}
+                                    <studip-tooltip-icon
+                                        :text="
+                                            $gettext(
+                                                'Nutzerdaten dürfen nur an das externe Tool gesendet werden, wenn es keine Datenschutzbedenken gibt. Mit Setzen des Hakens bestätigen Sie, dass die Übermittlung der Daten zulässig ist.'
+                                            )
+                                        "
+                                    />
+                                </label>
+                            </div>
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab :index="1" :name="$gettext('Zusätzliche Einstellungen')">
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Höhe') }}
+                                <input type="number" v-model="currentHeight" min="0" />
+                            </label>
+                            <label>
+                                {{ $gettext('Zusätzliche LTI-Parameter') }}
+                                <studip-tooltip-icon
+                                    :text="$gettext('Ein Wert pro Zeile, Beispiel: Review:Chapter=1.2.56')"
+                                />
+                                <textarea v-model="currentCustomParameters" />
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum LTI-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-lti-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentHeight: '',
+            currentToolId: '',
+            currentLaunchUrl: '',
+            currentConsumerKey: '',
+            currentConsumerSecret: '',
+            currentOauthSignatureMethod: '',
+            currentSendLisPerson: false,
+            currentCustomParameters: '',
+        };
+    },
+    computed: {
+        ...mapGetters({
+            urlHelper: 'urlHelper',
+            ltiTools: 'lti-tools/all',
+        }),
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        height() {
+            return this.block?.attributes?.payload?.height;
+        },
+        tools() {
+            return this.ltiTools.map((tool) => ({
+                id: tool.id,
+                name: tool.attributes.name,
+                launch_url: tool.attributes['launch-url'],
+                allow_custom_url: tool.attributes['allow-custom-url'],
+            }));
+        },
+        toolId() {
+            return this.block?.attributes?.payload?.tool_id;
+        },
+        currentTool() {
+            return this.tools.find((tool) => tool.id === this.currentToolId);
+        },
+        allowCustomUrl() {
+            return this.currentTool?.allow_custom_url;
+        },
+        customToolSelected() {
+            return this.currentToolId === '0';
+        },
+        launchUrl() {
+            return this.block?.attributes?.payload?.launch_url;
+        },
+        consumerKey() {
+            return this.block?.attributes?.payload?.consumer_key;
+        },
+        consumerSecret() {
+            return this.block?.attributes?.payload?.consumer_secret;
+        },
+        oauthSignatureMethod() {
+            return this.block?.attributes?.payload?.oauth_signature_method ?? 'sha1';
+        },
+        sendLisPerson() {
+            return this.block?.attributes?.payload?.send_lis_person;
+        },
+        customParameters() {
+            return this.block?.attributes?.payload?.custom_parameters;
+        },
+        iframeUrl() {
+            return this.urlHelper.getURL('dispatch.php/courseware/lti/iframe/' + this.block.id);
+        },
+    },
+    async mounted() {
+        await this.loadLtiTools();
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            loadLtiTools: 'lti-tools/loadAll',
+            companionWarning: 'companionWarning',
+        }),
+        initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentHeight = this.height;
+            this.currentToolId = this.toolId !== '' ? this.toolId : this.currentToolId; // keep preselected tool
+            this.currentLaunchUrl = this.launchUrl;
+            this.currentConsumerKey = this.consumerKey;
+            this.currentConsumerSecret = this.consumerSecret;
+            this.currentOauthSignatureMethod = this.oauthSignatureMethod;
+            this.currentSendLisPerson = Boolean(this.sendLisPerson); // prevent undefined value
+            this.currentCustomParameters = this.customParameters;
+        },
+        storeBlock() {
+            // require url, key and secret if custom tool is selected
+            if (this.currentToolId === '0') {
+                if (!this.currentLaunchUrl) {
+                    this.companionWarning({
+                        info: this.$gettext('Bitte geben Sie eine URL der Anwendung an.'),
+                    });
+                    return false;
+                }
+                if (!this.currentConsumerKey) {
+                    this.companionWarning({
+                        info: this.$gettext('Bitte geben Sie den Consumer-Key des LTI-Tools an.'),
+                    });
+                    return false;
+                }
+                if (!this.currentConsumerSecret) {
+                    this.companionWarning({
+                        info: this.$gettext('Bitte geben Sie den Consumer-Secret des LTI-Tools an.'),
+                    });
+                    return false;
+                }
+            }
+
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.title = this.currentTitle;
+            attributes.payload.height = this.currentHeight;
+            attributes.payload.tool_id = this.currentToolId;
+            attributes.payload.launch_url = this.currentLaunchUrl;
+            if (this.currentToolId === '0') {
+                attributes.payload.consumer_key = this.currentConsumerKey;
+                attributes.payload.consumer_secret = this.currentConsumerSecret;
+                attributes.payload.oauth_signature_method = this.currentOauthSignatureMethod;
+                attributes.payload.send_lis_person = this.currentSendLisPerson;
+            }
+            attributes.payload.custom_parameters = this.currentCustomParameters;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+    watch: {
+        tools(value) {
+            // Preselect tool
+            if (this.currentToolId === '') {
+                if (value.length > 0) {
+                    // Preselect first tool
+                    this.currentToolId = value[0].id;
+                } else {
+                    // Preselect custom tool
+                    this.currentToolId = '0';
+                }
+            }
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/lti.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewarePDFTableOfContent.vue b/resources/vue/components/courseware/blocks/CoursewarePDFTableOfContent.vue
new file mode 100644
index 0000000..989e2ed
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewarePDFTableOfContent.vue
@@ -0,0 +1,30 @@
+<template>
+    <li class="cw-pdf-toc-item">
+        <a href="#" @click.prevent="tocPageNav(item.dest)">
+            <strong v-if="item.bold">{{ item.title }}</strong>
+            <span v-else>{{ item.title }}</span>
+        </a>
+        <template v-if="item.items">
+            <ul class="cw-pdf-toc-sub-list">
+                <courseware-pdf-toc-item v-for="(item, index) in item.items" :item="item" :key="index" @tocPageNav="tocPageNav"></courseware-pdf-toc-item>
+            </ul>
+        </template>
+    </li>
+</template>
+
+<script>
+export default {
+    name: 'courseware-pdf-toc-item',
+    props: {
+        item: {
+            type: Object,
+            required: true,
+        },
+    },
+    methods: {
+        tocPageNav(dest) {
+            this.$emit('tocPageNav', dest);
+        }
+    },
+}
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue
new file mode 100644
index 0000000..ef111bf
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue
@@ -0,0 +1,251 @@
+<template>
+    <div class="cw-block cw-block-table-of-contents">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeText"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="childElementsWithTasks.length > 0">
+                    <div v-if="currentStyle !== 'tiles' && currentTitle !== ''" class="cw-block-title">
+                        {{ currentTitle }}
+                    </div>
+                    <ul
+                        v-if="currentStyle === 'list-details' || currentStyle === 'list'"
+                        :class="['cw-block-table-of-contents-' + currentStyle]"
+                    >
+                        <li v-for="child in childElementsWithTasks" :key="child.id">
+                            <router-link :to="'/structural_element/' + child.id">
+                                <div
+                                    class="cw-block-table-of-contents-title-box"
+                                    :class="[child.attributes.payload.color]"
+                                >
+                                    {{ child.attributes.title }}
+                                    <span v-if="child.attributes.purpose === 'task'"> | {{ child.solverName }}</span>
+                                    <p v-if="currentStyle === 'list-details'">
+                                        {{ child.attributes.payload.description }}
+                                    </p>
+                                </div>
+                            </router-link>
+                        </li>
+                    </ul>
+                    <ul v-if="currentStyle === 'tiles'" class="cw-block-table-of-contents-tiles cw-tiles">
+                        <li
+                            v-for="child in childElementsWithTasks"
+                            :key="child.id"
+                            class="tile"
+                            :class="[child.attributes.payload.color]"
+                        >
+                            <router-link
+                                :to="'/structural_element/' + child.id"
+                                :title="
+                                    child.attributes.purpose === 'task'
+                                        ? child.attributes.title + ' | ' + child.solverName
+                                        : child.attributes.title
+                                "
+                            >
+                                <div
+                                    class="preview-image"
+                                    :class="[hasImage(child) ? '' : 'default-image']"
+                                    :style="getChildStyle(child)"
+                                >
+                                    <div v-if="child.attributes.purpose === 'task'" class="overlay-text">
+                                        {{ child.solverName }}
+                                    </div>
+                                </div>
+                                <div class="description">
+                                    <header
+                                        :class="[
+                                            child.attributes.purpose !== ''
+                                                ? 'description-icon-' + child.attributes.purpose
+                                                : '',
+                                        ]"
+                                    >
+                                        {{ child.attributes.title || '–' }}
+                                    </header>
+                                    <div class="description-text-wrapper">
+                                        <p>{{ child.attributes.payload.description }}</p>
+                                    </div>
+                                    <footer>
+                                        {{ countChildChildren(child) }}
+                                        <translate :translate-n="countChildChildren(child)" translate-plural="Seiten">
+                                            Seite
+                                        </translate>
+                                    </footer>
+                                </div>
+                            </router-link>
+                        </li>
+                    </ul>
+                </div>
+                <courseware-companion-box
+                    v-if="viewMode === 'edit' && childElementsWithTasks.length === 0"
+                    :msgCompanion="
+                        $gettext(
+                            'Es sind noch keine Unterseiten vorhanden. ' +
+                                'Sobald Sie weitere Unterseiten anlegen, erscheinen diese automatisch hier im Inhaltsverzeichnis.'
+                        )
+                    "
+                    mood="pointing"
+                />
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Überschrift') }}
+                        <input type="text" v-model="currentTitle" />
+                    </label>
+                    <label>
+                        {{ $gettext('Layout') }}
+                        <select v-model="currentStyle">
+                            <option value="list">{{ $gettext('Liste') }}</option>
+                            <option value="list-details">{{ $gettext('Liste mit Beschreibung') }}</option>
+                            <option value="tiles">{{ $gettext('Kacheln') }}</option>
+                        </select>
+                    </label>
+                </form>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Inhaltsverzeichnis-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-table-of-contents-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentStyle: '',
+        };
+    },
+    computed: {
+        ...mapGetters({
+            childrenById: 'courseware-structure/children',
+            structuralElementById: 'courseware-structural-elements/byId',
+            context: 'context',
+            taskById: 'courseware-tasks/byId',
+            userById: 'users/byId',
+            groupById: 'status-groups/byId',
+            viewMode: 'viewMode',
+        }),
+        structuralElement() {
+            return this.structuralElementById({ id: this.$route.params.id });
+        },
+        childElements() {
+            return this.childrenById(this.structuralElement.id).map((id) => this.structuralElementById({ id }));
+        },
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        style() {
+            return this.block?.attributes?.payload?.style;
+        },
+        childElementsWithTasks() {
+            let children = [];
+            this.childElements.forEach((element) => {
+                if (element.relationships.task.data) {
+                    let solverName = this.getSolverName(element.relationships.task.data.id);
+                    if (solverName) {
+                        element.solverName = solverName;
+                        children.push(element);
+                    }
+                } else {
+                    children.push(element);
+                }
+            });
+
+            return children;
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+        this.childElements.forEach((element) => {
+            if (element.relationships.task.data) {
+                const taskId = element.relationships.task.data.id;
+                try {
+                    this.loadTask({
+                        taskId: taskId,
+                    });
+                } catch (error) {
+                    // nothing to do here
+                }
+            }
+        });
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            loadTask: 'loadTask',
+        }),
+        initCurrentData() {
+            this.currentTitle = this.title;
+            this.currentStyle = this.style;
+        },
+        storeText() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.title = this.currentTitle;
+            attributes.payload.style = this.currentStyle;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+        getChildStyle(child) {
+            let url = child.relationships?.image?.meta?.['download-url'];
+
+            if (url) {
+                return { 'background-image': 'url(' + url + ')' };
+            } else {
+                return {};
+            }
+        },
+        countChildChildren(child) {
+            return this.childrenById(child.id).length + 1;
+        },
+        hasImage(child) {
+            return child.relationships?.image?.data !== null;
+        },
+
+        getSolverName(taskId) {
+            const task = this.taskById({ id: taskId });
+            if (task === undefined) {
+                return false;
+            }
+            const solver = task.relationships.solver.data;
+            if (solver.type === 'users') {
+                const user = this.userById({ id: solver.id });
+
+                return user.attributes['formatted-name'];
+            }
+            if (solver.type === 'status-groups') {
+                const group = this.groupById({ id: solver.id });
+
+                return group.attributes.name;
+            }
+
+            return false;
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/table-of-contents.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue
new file mode 100644
index 0000000..f8f18bc
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue
@@ -0,0 +1,115 @@
+<template>
+    <div class="cw-block cw-block-text">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="false"
+            ref="defaultBlock"
+            @showEdit="initCurrent"
+            @storeEdit="storeText"
+            @closeEdit="initCurrent"
+        >
+            <template #content>
+                <section class="formatted-content ck-content" v-html="currentText" ref="content"></section>
+            </template>
+            <template v-if="canEdit" #edit>
+                <ckeditor :editor="editor" v-model="currentText" :config="editorConfig" @ready="onReady"></ckeditor>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Text-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { ClassicEditor, BalloonEditor } from '@/assets/javascripts/chunks/wysiwyg';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-text-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentText: '',
+            editor: ClassicEditor,
+            editorConfig: {
+                // The configuration of the editor.
+            },
+        };
+    },
+    computed: {
+        text() {
+            return this.block?.attributes?.payload?.text;
+        },
+        ckeToolbarTop() {
+            const topBar = document.getElementById('top-bar');
+            const responsiveContentbar = document.getElementById('responsive-contentbar');
+            let top = topBar.clientHeight + topBar.clientTop;
+            if (responsiveContentbar) {
+                top += responsiveContentbar?.clientHeight + responsiveContentbar?.clientTop;
+            } else {
+                top += 85;
+            }
+
+            return top;
+        },
+    },
+    mounted() {
+        this.initCurrent();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrent() {
+            this.currentText = this.text;
+            this.loadMathjax();
+        },
+        onReady(editor) {
+            editor.ui.viewportOffset = { top: this.ckeToolbarTop };
+            editor.ui.update();
+        },
+        async storeText() {
+            let attributes = this.block.attributes;
+            attributes.payload.text = this.currentText;
+            await this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+            this.$refs.defaultBlock.displayFeature(false);
+            this.loadMathjax();
+        },
+        loadMathjax() {
+            let mathjaxP;
+            let view = this;
+
+            if (window.MathJax && window.MathJax.Hub) {
+                mathjaxP = Promise.resolve(window.MathJax);
+            } else if (window.STUDIP && window.STUDIP.loadChunk) {
+                mathjaxP = window.STUDIP.loadChunk('mathjax');
+            }
+
+            mathjaxP &&
+                mathjaxP
+                    .then(({ Hub }) => {
+                        Hub.Queue(['Typeset', Hub, view.$refs.content]);
+                    })
+                    .catch(() => {
+                        console.log('Warning: Could not load MathJax.');
+                    });
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/text.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareTimelineBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTimelineBlock.vue
new file mode 100644
index 0000000..4e93e70
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareTimelineBlock.vue
@@ -0,0 +1,307 @@
+<template>
+    <div class="cw-block cw-block-timeline">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @closeEdit="initCurrentData"
+            @showEdit="setShowEdit"
+            @storeEdit="storeBlock"
+        >
+            <template #content>
+                <ol class="cw-timeline">
+                    <li
+                        v-for="(item, index) in sortedItems"
+                        :key="index"
+                        class="cw-timeline-item"
+                    >
+                        <div class="cw-timeline-item-icon" :class="'cw-timeline-item-icon-color-' + item.color">
+                            <studip-icon :shape="item.icon" role="info" size="32" :class="item.color"/>
+                        </div>
+                        <div
+                            class="cw-timeline-item-content"
+                            :class="[index % 2 === 0 ? 'left' : 'right', 'cw-timeline-item-content-color-' + item.color]"
+                        >
+                            <h3 v-if="currentDateFormat !== 'none'">{{ getItemDate(item) }}</h3>
+                            <h3 v-else>{{ item.title }}</h3>
+                            <article>
+                                <header v-if="currentDateFormat !== 'none'">{{ item.title }}</header>
+                                <p>{{ item.description }}</p>
+                            </article>
+                        </div>
+                    </li>
+                </ol>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label class="col-2">
+                        {{ $gettext('Zeitliche Sortierung') }}
+                        <select v-model="currentSort">
+                            <option value="none">{{ $gettext('Keine') }}</option>
+                            <option value="asc">{{ $gettext('Aufsteigend') }}</option>
+                            <option value="desc">{{ $gettext('Absteigend') }}</option>
+                        </select>
+                    </label>
+                    <label class="col-2">
+                        {{ $gettext('Zeitangabe') }}
+                        <select v-model="currentDateFormat">
+                            <option value="year">{{ $gettext('Jahr') }}</option>
+                            <option value="date">{{ $gettext('Datum') }}</option>
+                            <option value="time">{{ $gettext('Zeit') }}</option>
+                            <option value="datetime">{{ $gettext('Datum und Zeit') }}</option>
+                            <option value="none">{{ $gettext('Keine') }}</option>
+                        </select>
+                    </label>
+                </form>
+                <button class="button add" @click="addItem">{{ $gettext('Ereignis hinzufügen') }}</button>
+                <courseware-tabs
+                    v-if="currentItems.length > 0"
+                    :setSelected="setItemTab"
+                    @selectTab="setItemTab = (parseInt($event.name.replace($gettext('Ereignis') +  ' ', '')) - 1)"
+                >
+                    <courseware-tab
+                        v-for="(item, index) in currentItems"
+                        :key="index"
+                        :index="index"
+                        :name="$gettext('Ereignis') +  ' ' + (index + 1).toString()"
+                        :selected="index === 0"
+                        canBeEmpty
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label class="col-4">
+                                {{ $gettext('Titel') }}
+                                <input type="text" v-model="item.title" />
+                            </label>
+                            <label class="col-4">
+                            
+                                {{ $gettext('Beschreibung') }}
+                                <textarea v-model="item.description" />
+                            </label>
+                            <br>
+                            <label class="col-2">
+                                {{ $gettext('Farbe') }}
+                                <studip-select
+                                    :options="colors"
+                                    label="icon"
+                                    :clearable="false"
+                                    :reduce="option => option.class"
+                                    v-model="item.color"
+                                >
+                                    <template #open-indicator="selectAttributes">
+                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
+                                    </template>
+                                    <template #no-options>
+                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                                    </template>
+                                    <template #selected-option="{name, hex}">
+                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
+                                    </template>
+                                    <template #option="{name, hex}">
+                                        <span class="vs__option-color" :style="{'background-color': hex}"></span><span>{{name}}</span>
+                                    </template>
+                                </studip-select>
+                            </label>
+                            <label class="col-2">
+                                {{ $gettext('Icon') }}
+                                <studip-select :options="icons" :clearable="false" v-model="item.icon">
+                                    <template #open-indicator="selectAttributes">
+                                        <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
+                                    </template>
+                                    <template #no-options>
+                                        {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                                    </template>
+                                    <template #selected-option="option">
+                                        <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
+                                    </template>
+                                    <template #option="option">
+                                        <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
+                                    </template>
+                                </studip-select>
+                            </label>
+                            <br>
+                            <label class="col-1">
+                                {{ $gettext('Datum') }}
+                                <input type="date" v-model="item.date" required/>
+                            </label>
+                            <label class="col-1">
+                                {{ $gettext('Zeit') }}
+                                <input type="time" v-model="item.time" />
+                            </label>
+                            <label v-if="currentItems.length > 1">
+                                <button class="button trash" @click="removeItem(index)">
+                                    {{ $gettext('Ereignis entfernen') }}
+                                </button>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Zeitstrahl-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import contentIconsMixin from '@/vue/mixins/courseware/content-icons.js';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-timeline-block',
+    mixins: [blockMixin, colorMixin, contentIconsMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            showEdit: false,
+            setItemTab: 0,
+            currentItems: [],
+            currentSort: '',
+            currentDateFormat: '',
+        }
+    },
+    computed: {
+        items() {
+            return this.block?.attributes?.payload?.items;
+        },
+        sort() {
+            return this.block?.attributes?.payload?.sort;
+        },
+        dateformat() {
+            return this.block?.attributes?.payload?.dateformat;
+        },
+        icons() {
+            return this.contentIcons;
+        },
+        colors() {
+            return this.mixinColors.filter(color => color.class !== 'white' && color.class !== 'studip-lightblue');
+        },
+        sortedItems() {
+            if (this.currentSort === 'none') {
+                return this.currentItems;
+            }
+            let view = this;
+            let items = _.cloneDeep(this.currentItems);
+            return items.sort((a, b) => {
+                let dateA = null;
+                let dateB = null;
+
+                if (a.time) {
+                    dateA = new Date(a.date + 'T' + a.time);
+                } else {
+                    dateA = new Date(a.date);
+                }
+                if (b.time) {
+                    dateB = new Date(b.date + 'T' + b.time);
+                } else {
+                    dateB = new Date(b.date);
+                }
+                if (view.currentSort === 'asc') {
+                    return dateA > dateB ? 1 : dateA < dateB ? -1 : 0;
+                }
+                if (view.currentSort === 'desc') {
+                    return dateA < dateB ? 1 : dateA > dateB ? -1 : 0;
+                }
+            });
+        }
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
+        initCurrentData() {
+            this.currentItems = this.items;
+            this.currentSort = this.sort;
+            this.currentDateFormat = this.dateformat;
+            this.setItemTab = 0;
+        },
+        getItemDate(item) {
+            switch (this.currentDateFormat) {
+                case 'year':
+                    if (item.date) {
+                        return (new Date(item.date)).getFullYear();
+                    }
+                    break;
+                case 'date':
+                    if (item.date) {
+                        return this.getReadableDate(item.date);
+                    }
+                    break;
+                case 'time':
+                     if (item.time) {
+                        return item.time;
+                     }
+                     break;
+                case 'datetime':
+                    if (item.date && item.time) {
+                        return this.getReadableDate(item.date) + ' ' + item.time;
+                    }
+                    if (!item.date && item.time) {
+                        return '--.--.---- ' + item.time;
+                    }
+                    if (item.date && !item.time) {
+                        return this.getReadableDate(item.date) + ' --:--';
+                    }
+                    return '--.--.---- --:--'
+            }
+
+            return '';
+        },
+        addItem() {
+            this.currentItems.push({
+                title: '',
+                description: '',
+                date: '',
+                time: '',
+                color: 'studip-blue',
+                icon: 'courseware'
+            });
+            this.$nextTick(() => { this.setItemTab = this.currentItems.length - 1; });
+        },
+        removeItem(itemIndex){
+            this.currentItems = this.currentItems.filter((val, index) => {
+                return !(index === itemIndex);
+            });
+            this.$nextTick(() => { this.setItemTab = 0; });
+        },
+        storeBlock() {
+            let attributes = {
+                payload: {
+                    dateformat: this.currentDateFormat,
+                    sort: this.currentSort,
+                    items: this.currentItems
+                }
+            };
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+    watch: {
+        items() {
+            if (!this.showEdit) {
+                this.initCurrentData();
+            }
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/timeline.scss';
+</style>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareTypewriterBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTypewriterBlock.vue
new file mode 100644
index 0000000..ab7f7c5
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareTypewriterBlock.vue
@@ -0,0 +1,156 @@
+<template>
+    <div class="cw-block cw-block-typewriter">
+        <courseware-default-block
+        :block="block"
+        :canEdit="canEdit"
+        :isTeacher="isTeacher"
+        :preview="true"
+        @showEdit="initCurrentData"
+        @storeEdit="storeText"
+        @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div class="cw-typewriter-content">
+                    <vue-typer
+                        :text="currentText"
+                        initial-action="typing"
+                        :repeat="0"
+                        :type-delay="typeDelay"
+                        caret-animation="smooth"
+                        :class="[currentFont, currentSize]"
+                    ></vue-typer>
+                </div>
+            </template>
+            <template v-if="canEdit" #edit>
+                <form class="default" @submit.prevent="">
+                    <label class="col-4">
+                        {{ $gettext('Text') }}
+                        <textarea v-model="currentText" />
+                    </label>
+                    <br>
+                    <label class="col-1">
+                        {{ $gettext('Geschwindigkeit') }}
+                        <select v-model="currentSpeed" @change="restartTyping">
+                            <option value="0">{{ $gettext('Langsam') }}</option>
+                            <option value="1">{{ $gettext('Normal') }}</option>
+                            <option value="2">{{ $gettext('Schnell') }}</option>
+                            <option value="3">{{ $gettext('Sehr schnell') }}</option>
+                        </select>
+                    </label>
+                    <label class="col-1">
+                        {{ $gettext('Schriftart') }}
+                        <select v-model="currentFont">
+                            <option value="font-default">{{ $gettext('Standard') }}</option>
+                            <option value="font-typewriter">Lucida Sans Typewriter</option>
+                            <option value="font-trebuchet">Trebuchet MS</option>
+                            <option value="font-tahoma">Tahoma</option>
+                            <option value="font-georgia">Georgia</option>
+                            <option value="font-narrow">Arial Narrow</option>
+                        </select>
+                    </label>
+                    <label class="col-1">
+                        {{ $gettext('Schriftgröße') }}
+                        <select v-model="currentSize">
+                            <option value="size-default">100%</option>
+                            <option value="size-tall">125%</option>
+                            <option value="size-grande">150%</option>
+                            <option value="size-huge">200%</option>
+                        </select>
+                    </label>
+                </form>
+            </template>
+            <template #info>
+                <p>{{ $gettext('Informationen zum Schreibmaschinen-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { VueTyper } from 'vue-typer';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-typewriter-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, { VueTyper }),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            speeds: [200, 100, 50, 25],
+            typing: false,
+            speedClasses: [
+                'cw-typewriter-letter-fadein-slow',
+                'cw-typewriter-letter-fadein-normal',
+                'cw-typewriter-letter-fadein-fast',
+                'cw-typewriter-letter-fadein-veryfast',
+            ],
+            currentText: ' ',
+            currentSpeed: '',
+            currentFont: '',
+            currentSize: '',
+        };
+    },
+    computed: {
+        text() {
+            return this.block?.attributes?.payload?.text;
+        },
+        speed() {
+            return this.block?.attributes?.payload?.speed;
+        },
+        typeDelay() {
+            return this.speeds[this.currentSpeed];
+        },
+        font() {
+            return this.block?.attributes?.payload?.font;
+        },
+        size() {
+            return this.block?.attributes?.payload?.size;
+        }
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+        }),
+        initCurrentData() {
+            this.currentText = this.text;
+            this.currentSpeed = this.speed;
+            this.currentFont = this.font;
+            this.currentSize = this.size;
+        },
+        restartTyping() {
+            let text = this.currentText;
+            this.currentText = ' ';
+            this.$nextTick(()=> {
+                this.currentText = text;
+            });
+        },
+        storeText() {
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.text = this.currentText;
+            attributes.payload.speed = this.currentSpeed;
+            attributes.payload.font = this.currentFont;
+            attributes.payload.size = this.currentSize;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        }
+    },
+};
+</script>
+<style scoped lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/typewriter.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue b/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue
new file mode 100644
index 0000000..2ed3074
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue
@@ -0,0 +1,241 @@
+<template>
+    <div class="cw-block cw-block-video">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @showEdit="initCurrentData"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentTitle !== '' && currentURL" class="cw-block-title">{{ currentTitle }}</div>
+                <video
+                    v-show="currentURL"
+                    :src="currentURL"
+                    :type="currentFile !== '' ? currentFile.mime_type : ''"
+                    controls
+                    :autoplay="currentAutoplay === 'enabled'"
+                    @contextmenu="contextHandler"
+                />
+            </template>
+            <template v-if="canEdit" #edit>
+                <courseware-tabs>
+                    <courseware-tab
+                        :index="0"
+                        :name="$gettext('Grunddaten')"
+                        :selected="true"
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Überschrift') }}
+                                <input type="text" v-model="currentTitle" />
+                            </label>
+                            <label>
+                                {{ $gettext('Quelle') }}
+                                <select v-model="currentSource">
+                                    <option value="studip">{{ $gettext('Dateibereich') }}</option>
+                                    <option value="web">{{ $gettext('Web-Adresse') }}</option>
+                                </select>
+                            </label>
+                            <label v-show="currentSource === 'web'">
+                                {{ $gettext('URL') }}
+                                <input type="text" v-model="currentWebUrl" />
+                            </label>
+                            <label v-show="currentSource === 'studip'">
+                                {{ $gettext('Datei') }}
+                                <courseware-file-chooser
+                                    v-model="currentFileId"
+                                    :isVideo="true"
+                                    @selectFile="updateCurrentFile"
+                                />
+                            </label>
+
+                        </form>
+                    </courseware-tab>
+                    <courseware-tab
+                        :index="1"
+                        :name="$gettext('Video Einstellungen')"
+                    >
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                {{ $gettext('Seitenverhältnis') }}
+                                <select v-model="currentAspect">
+                                    <option value="169">16:9</option>
+                                    <option value="43">4:3</option>
+                                </select>
+                            </label>
+                            <label>
+                                {{ $gettext('Video startet automatisch') }}
+                                <select v-model="currentAutoplay">
+                                    <option value="disabled">{{ $gettext('Nein') }}</option>
+                                    <option value="enabled">{{ $gettext('Ja') }}</option>
+                                </select>
+                            </label>
+                            <label>
+                                {{ $gettext('Contextmenü') }}
+                                <select v-model="currentContextMenu">
+                                    <option value="enabled">{{ $gettext('Erlauben') }}</option>
+                                    <option value="disabled">{{ $gettext('Verhindern') }}</option>
+                                </select>
+                            </label>
+                        </form>
+                    </courseware-tab>
+                </courseware-tabs>
+            </template>
+            <template #info>{{ $gettext('Informationen zum Video-Block') }}</template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-video-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, {}),
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentSource: '',
+            currentTitle: '',
+            currentFile: {},
+            currentFileId: '',
+            currentAspect: '',
+            currentContextMenu: '',
+            currentAutoplay: '',
+            currentWebUrl: '',
+        };
+    },
+    computed: {
+        ...mapGetters({
+            fileRefById: 'file-refs/byId',
+            urlHelper: 'urlHelper',
+        }),
+        title() {
+            return this.block?.attributes?.payload?.title;
+        },
+        source() {
+            return this.block?.attributes?.payload?.source;
+        },
+        fileId() {
+            return this.block?.attributes?.payload?.file_id;
+        },
+        webUrl() {
+            return this.block?.attributes?.payload?.web_url;
+        },
+        aspect() {
+            return this.block?.attributes?.payload?.aspect;
+        },
+        contextMenu() {
+            return this.block?.attributes?.payload?.context_menu;
+        },
+        autoplay() {
+            return this.block?.attributes?.payload?.autoplay;
+        },
+        currentURL() {
+            if (this.currentSource === 'studip' && this.currentFile) {
+                return this.currentFile.download_url;
+            }
+            if (this.currentSource === 'web') {
+                return this.currentWebUrl;
+            }
+            return false;
+        },
+
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            loadFileRef: 'file-refs/loadById',
+            companionWarning: 'companionWarning',
+        }),
+        storeBlock() {
+            let cmpInfo = false;
+            let attributes = {};
+            attributes.payload = {};
+            attributes.payload.title = this.currentTitle;
+            attributes.payload.source = this.currentSource;
+            if (this.currentSource === 'studip' && this.currentFile !== undefined && this.currentFileId !== '') {
+                attributes.payload.file_id = this.currentFile.id;
+                attributes.payload.web_url = '';
+            } else if (this.currentSource === 'web' && this.currentWebUrl !== '') {
+                attributes.payload.file_id = '';
+                attributes.payload.web_url = this.currentWebUrl;
+            } else {
+                cmpInfo = this.$gettext('Bitte wählen Sie ein Video aus.');
+            }
+            attributes.payload.aspect = this.currentAspect;
+            attributes.payload.context_menu = this.currentContextMenu;
+            attributes.payload.autoplay = this.currentAutoplay;
+
+            if (cmpInfo) {
+                this.companionWarning({ info: cmpInfo });
+                return false;
+            }
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+        async initCurrentData() {
+            this.currentSource = this.source;
+            this.currentTitle = this.title;
+            this.currentWebUrl = this.webUrl;
+            this.currentFileId = this.fileId;
+            this.currentAspect = this.aspect;
+            this.currentContextMenu = this.contextMenu;
+            this.currentAutoplay = this.autoplay;
+            if (this.fileId !== '') {
+                await this.loadFile();
+            }
+        },
+        async loadFile() {
+            const id = this.currentFileId;
+            await this.loadFileRef({ id });
+            const fileRef = this.fileRefById({ id });
+
+            if (fileRef) {
+                this.updateCurrentFile({
+                    id: fileRef.id,
+                    name: fileRef.attributes.name,
+                    download_url: this.urlHelper.getURL(
+                        'sendfile.php',
+                        { type: 0, file_id: fileRef.id, file_name: fileRef.attributes.name },
+                        true
+                    ),
+                });
+            }
+        },
+        updateCurrentFile(file) {
+            this.currentFile = file;
+            this.currentFileId = file.id;
+        },
+        contextHandler(e) {
+            if (this.currentContextMenu === 'disabled') {
+                e.preventDefault();
+            }
+        },
+    },
+};
+</script>
+<style scoped lang="scss">
+.cw-block-video {
+    video {
+        width: 100%;
+    }
+}
+</style>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/block-components.js b/resources/vue/components/courseware/blocks/block-components.js
new file mode 100644
index 0000000..a79ed4a
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/block-components.js
@@ -0,0 +1,18 @@
+import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
+import CoursewareFileChooser from '../layouts/CoursewareFileChooser.vue';
+import CoursewareFolderChooser from '../layouts/CoursewareFolderChooser.vue';
+import CoursewareTabs from '../layouts/CoursewareTabs.vue';
+import CoursewareTab from '../layouts/CoursewareTab.vue';
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'
+
+const BlockComponents = {
+    CoursewareDefaultBlock,
+    CoursewareFileChooser,
+    CoursewareFolderChooser,
+
+    CoursewareTabs,
+    CoursewareTab,
+    CoursewareCompanionBox,
+}
+
+export default BlockComponents;
\ No newline at end of file
diff --git a/resources/vue/components/courseware/container-components.js b/resources/vue/components/courseware/container-components.js
deleted file mode 100644
index 63e6658..0000000
--- a/resources/vue/components/courseware/container-components.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareDefaultContainer from './CoursewareDefaultContainer.vue';
-import CoursewareBlockAdderArea from './CoursewareBlockAdderArea.vue';
-// blocks
-import CoursewareAudioBlock from './CoursewareAudioBlock.vue';
-import CoursewareBeforeAfterBlock from './CoursewareBeforeAfterBlock.vue';
-import CoursewareCanvasBlock from './CoursewareCanvasBlock.vue';
-import CoursewareChartBlock from './CoursewareChartBlock.vue';
-import CoursewareCodeBlock from './CoursewareCodeBlock.vue';
-import CoursewareConfirmBlock from './CoursewareConfirmBlock.vue';
-import CoursewareDateBlock from './CoursewareDateBlock.vue';
-import CoursewareDialogCardsBlock from './CoursewareDialogCardsBlock.vue';
-import CoursewareDocumentBlock from './CoursewareDocumentBlock.vue';
-import CoursewareDownloadBlock from './CoursewareDownloadBlock.vue';
-import CoursewareEmbedBlock from './CoursewareEmbedBlock.vue';
-import CoursewareErrorBlock from './CoursewareErrorBlock.vue';
-import CoursewareFolderBlock from './CoursewareFolderBlock.vue';
-import CoursewareGalleryBlock from './CoursewareGalleryBlock.vue';
-import CoursewareHeadlineBlock from './CoursewareHeadlineBlock.vue';
-import CoursewareIframeBlock from './CoursewareIframeBlock.vue';
-import CoursewareImageMapBlock from './CoursewareImageMapBlock.vue';
-import CoursewareKeyPointBlock from './CoursewareKeyPointBlock.vue';
-import CoursewareLinkBlock from './CoursewareLinkBlock.vue';
-import CoursewareLtiBlock from "./CoursewareLtiBlock.vue";
-import CoursewareTableOfContentsBlock from './CoursewareTableOfContentsBlock.vue';
-import CoursewareTextBlock from './CoursewareTextBlock.vue';
-import CoursewareTimelineBlock from './CoursewareTimelineBlock.vue';
-import CoursewareTypewriterBlock from './CoursewareTypewriterBlock.vue';
-import CoursewareVideoBlock from './CoursewareVideoBlock.vue';
-import CoursewareBiographyAchievementsBlock from './CoursewareBiographyAchievementsBlock.vue';
-import CoursewareBiographyCareerBlock from './CoursewareBiographyCareerBlock.vue';
-import CoursewareBiographyGoalsBlock from './CoursewareBiographyGoalsBlock.vue';
-import CoursewareBiographyPersonalInformationBlock from './CoursewareBiographyPersonalInformationBlock.vue';
-
-
-const ContainerComponents = {
-    CoursewareDefaultBlock,
-    CoursewareDefaultContainer,
-    CoursewareBlockAdderArea,
-    // blocks
-    CoursewareAudioBlock,
-    CoursewareBeforeAfterBlock,
-    CoursewareCanvasBlock,
-    CoursewareChartBlock,
-    CoursewareCodeBlock,
-    CoursewareConfirmBlock,
-    CoursewareDateBlock,
-    CoursewareDialogCardsBlock,
-    CoursewareDocumentBlock,
-    CoursewareDownloadBlock,
-    CoursewareEmbedBlock,
-    CoursewareErrorBlock,
-    CoursewareFolderBlock,
-    CoursewareGalleryBlock,
-    CoursewareHeadlineBlock,
-    CoursewareIframeBlock,
-    CoursewareImageMapBlock,
-    CoursewareKeyPointBlock,
-    CoursewareLinkBlock,
-    CoursewareLtiBlock,
-    CoursewareTableOfContentsBlock,
-    CoursewareTextBlock,
-    CoursewareTimelineBlock,
-    CoursewareTypewriterBlock,
-    CoursewareVideoBlock,
-    CoursewareBiographyAchievementsBlock,
-    CoursewareBiographyCareerBlock,
-    CoursewareBiographyGoalsBlock,
-    CoursewareBiographyPersonalInformationBlock,
-};
-
-export default ContainerComponents;
diff --git a/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue
new file mode 100644
index 0000000..33f515a
--- /dev/null
+++ b/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue
@@ -0,0 +1,403 @@
+<template>
+    <courseware-default-container
+        :container="container"
+        containerClass="cw-container-accordion"
+        :canEdit="canEdit"
+        :isTeacher="isTeacher"
+        @showEdit="setShowEdit"
+        @storeContainer="storeContainer"
+        @closeEdit="initCurrentData"
+    >
+        <template v-slot:containerContent>
+            <template v-if="showEditMode && canEdit && !currentElementisLink">
+                <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
+                <span id="operation" class="assistive-text">
+                    {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
+                </span>
+            </template>
+            <courseware-collapsible-box
+                v-for="(section, index) in currentSections"
+                :key="index"
+                :title="section.name"
+                :icon="section.icon"
+                :open="index === 0 || sortInSlots.includes(index)"
+            >
+                <ul v-if="!showEditMode || currentElementisLink" class="cw-container-accordion-block-list">
+                    <li v-for="block in section.blocks" :key="block.id" class="cw-block-item">
+                        <component
+                            :is="component(block)"
+                            :block="block"
+                            :canEdit="canEdit"
+                            :isTeacher="isTeacher"
+                        />
+                    </li>
+                </ul>
+                <template v-else>
+                    <template v-if="!processing">
+                        <draggable
+                            v-if="canEdit"
+                            class="cw-container-list-block-list cw-container-list-sort-mode"
+                            :class="[section.blocks.length === 0 ? 'cw-container-list-sort-mode-empty' : '']"
+                            tag="ol"
+                            role="listbox"
+                            v-model="section.blocks"
+                            v-bind="dragOptions"
+                            handle=".cw-sortable-handle"
+                            group="blocks"
+                            @start="isDragging = true"
+                            @end="dropBlock"
+                            :containerId="container.id"
+                            :sectionId="index"
+                        >
+                            <li v-for="block in section.blocks" :key="block.id" class="cw-block-item cw-block-item-sortable">
+                                <span
+                                    :class="{ 'cw-sortable-handle-dragging': isDragging }"
+                                    class="cw-sortable-handle"
+                                    tabindex="0"
+                                    role="button"
+                                    aria-describedby="operation"
+                                    :ref="'sortableHandle' + block.id"
+                                    @keydown="keyHandler($event, block.id, index)"
+                                ></span>
+                                <component
+                                    :is="component(block)"
+                                    :block="block"
+                                    :canEdit="canEdit"
+                                    :isTeacher="isTeacher"
+                                    :class="{ 'cw-block-item-selected': keyboardSelected === block.id}"
+                                    :blockId="block.id"
+                                />
+                            </li>
+                        </draggable>
+                        <template v-if="canAddElements">
+                            <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/>
+                        </template>
+                    </template>
+                    <div v-else class="progress-wrapper">
+                        <studip-progress-indicator :description="$gettext('Vorgang wird bearbeitet...')" />
+                    </div>
+                </template>
+            </courseware-collapsible-box>
+        </template>
+        <template v-slot:containerEditDialog>
+            <form class="default cw-container-dialog-edit-form" @submit.prevent="">
+                <fieldset v-for="(section, index) in currentContainer.attributes.payload.sections.filter(section => !section.locked)" :key="index">
+                    <label>
+                        <translate>Title</translate>
+                        <input type="text" v-model="section.name" />
+                    </label>
+                    <label>
+                        <translate>Icon</translate>
+                        <studip-select :options="icons" v-model="section.icon">
+                            <template #open-indicator="selectAttributes">
+                                <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
+                            </template>
+                            <template #no-options>
+                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
+                            </template>
+                            <template #selected-option="option">
+                                <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
+                            </template>
+                            <template #option="option">
+                                <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
+                            </template>
+                        </studip-select>
+                    </label>
+                    <label
+                        class="cw-container-section-delete"
+                        v-if="currentContainer.attributes.payload.sections.length > 1"
+                    >
+                    <button class="button trash" @click="deleteSection(index)"><translate>Fach löschen</translate></button>
+                    </label>
+                </fieldset>
+            </form>
+            <button class="button add" @click="addSection"><translate>Fach hinzufügen</translate></button>
+        </template>
+    </courseware-default-container>
+</template>
+
+<script>
+import ContainerComponents from './container-components.js';
+import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue';
+import containerMixin from '@/vue/mixins/courseware/container.js';
+import contentIconsMixin from '@/vue/mixins/courseware/content-icons.js';
+
+import { mapGetters, mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-accordion-container',
+    mixins: [containerMixin, contentIconsMixin],
+    components: Object.assign(ContainerComponents, {
+        CoursewareCollapsibleBox,
+    }),
+    props: {
+        container: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+        canAddElements: Boolean,
+    },
+    data() {
+        return {
+            showEdit: false,
+            currentContainer: {},
+            currentSections: [],
+            unallocatedBlocks: [],
+            sortMode: false,
+            isDragging: false,
+            dragOptions: {
+                animation: 0,
+                group: this.container.id,
+                disabled: false,
+                ghostClass: "block-ghost"
+            },
+            processing: false,
+            keyboardSelected: null,
+            sortInSlots: [],
+            assistiveLive: ''
+        };
+    },
+    computed: {
+        ...mapGetters({
+            blockById: 'courseware-blocks/byId',
+            viewMode: 'viewMode',
+            currentElementisLink: 'currentElementisLink'
+        }),
+        showEditMode() {
+            return this.viewMode === 'edit';
+        },
+        blocks() {
+            if (!this.container) {
+                return [];
+            }
+
+            return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })).filter((a) => a);
+        },
+        icons() {
+            return this.contentIcons;
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateContainer: 'updateContainer',
+            loadContainer: 'courseware-containers/loadById',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+        }),
+        initCurrentData() {
+            this.currentContainer = _.cloneDeep(this.container);
+
+            let view = this;
+            let sections = this.currentContainer.attributes.payload.sections;
+
+            const unallocated = new Set(this.blocks.map(({ id }) => id));
+
+            for (let section of sections) {
+                section.locked = false;
+                section.blocks = section.blocks.map((id) =>  view.blockById({id})).filter(Boolean);
+                for (let sectionBlock of section.blocks) {
+                    if (sectionBlock?.id && unallocated.has(sectionBlock.id)) {
+                        unallocated.delete(sectionBlock.id);
+                    }
+                }
+            }
+
+            if (unallocated.size > 0) {
+                this.unallocatedBlocks = [...unallocated].map((id) => view.blockById({ id }));
+                sections.push({
+                    blocks: this.unallocatedBlocks,
+                    name: this.$gettext('nicht zugewiesene Inhalte'),
+                    icon: 'decline',
+                    locked: true
+                });
+            }
+
+            this.currentSections = sections;
+            this.sortInSlots = [];
+        },
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
+        addSection() {
+            this.currentContainer.attributes.payload.sections.push({ name: '', icon: '', blocks: [] });
+        },
+        deleteSection(index) {
+            if (this.currentContainer.attributes.payload.sections.length === 1) {
+                return;
+            }
+            if (this.currentContainer.attributes.payload.sections[index].blocks.length > 0) {
+                if (index === 0) {
+                    this.currentContainer.attributes.payload.sections[
+                        index + 1
+                    ].blocks = this.currentContainer.attributes.payload.sections[index + 1].blocks.concat(
+                        this.currentContainer.attributes.payload.sections[index].blocks
+                    );
+                } else {
+                    this.currentContainer.attributes.payload.sections[
+                        index - 1
+                    ].blocks = this.currentContainer.attributes.payload.sections[index - 1].blocks.concat(
+                        this.currentContainer.attributes.payload.sections[index].blocks
+                    );
+                }
+            }
+            this.currentContainer.attributes.payload.sections.splice(index, 1);
+        },
+        async storeContainer() {
+            const timeout = setTimeout(() => this.processing = true, 800);
+            this.currentContainer.attributes.payload.sections = this.currentContainer.attributes.payload.sections.filter(section => !section.locked);
+            this.currentContainer.attributes.payload.sections.forEach(section => {
+                section.blocks = section.blocks.map((block) => {return block.id;});
+                delete section.locked;
+            });
+            await this.updateContainer({
+                container: this.currentContainer,
+                structuralElementId: this.currentContainer.relationships['structural-element'].data.id,
+            });
+            await this.unlockObject({ id: this.currentContainer.id, type: 'courseware-containers' });
+            await this.loadContainer({id : this.container.id });
+            this.initCurrentData();
+            clearTimeout(timeout);
+            this.processing = false;
+        },
+        async storeSort() {
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
+                this.loadContainer({id : this.container.id });
+                return false;
+            }
+            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
+            await this.storeContainer();
+            this.assistiveLive = '';
+        },
+
+        component(block) {
+            if (block.attributes) {
+                return 'courseware-' + block.attributes["block-type"] + '-block';
+            }
+            return null;
+        },
+        updateContent(blockAdder) {
+            if (blockAdder.container !== undefined && blockAdder.container.id === this.container.id) {
+                this.initCurrentData();
+            }
+        },
+        keyHandler(e, blockId, sectionIndex) {
+            switch (e.keyCode) {
+                case 27: // esc
+                    this.abortKeyboardSorting(blockId, sectionIndex);
+                    break;
+                case 13: // enter
+                    e.preventDefault();
+                    if (this.keyboardSelected) {
+                        this.storeKeyboardSorting(blockId, sectionIndex);
+                    } else {
+                        this.keyboardSelected = blockId;
+                        const block = this.blockById({id: blockId});
+                        const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
+                        this.assistiveLive =
+                            this.$gettextInterpolate(
+                                this.$gettext('%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
+                                , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
+                            );
+                    }
+                    break;
+            }
+            if (this.keyboardSelected) {
+                switch (e.keyCode) {
+                    case 9: //tab
+                        this.abortKeyboardSorting(blockId, sectionIndex);
+                        break;
+                    case 38: // up
+                        e.preventDefault();
+                        this.moveItemUp(blockId, sectionIndex);
+                        break;
+                    case 40: // down
+                        e.preventDefault();
+                        this.moveItemDown(blockId, sectionIndex);
+                        break;
+                }
+            }
+        },
+        moveItemUp(blockId, sectionIndex) {
+            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
+            const block = this.blockById({id: blockId});
+            if (currentIndex !== 0) {
+                const newPos = currentIndex - 1;
+                this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
+                this.assistiveLive =
+                    this.$gettextInterpolate(
+                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
+                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
+                    );
+            } else if (sectionIndex !== 0) {
+                const newSectionIndex = sectionIndex - 1;
+                if (!this.sortInSlots.includes(newSectionIndex)) {
+                    this.sortInSlots.push(newSectionIndex);
+                }
+                this.currentSections[newSectionIndex].blocks.push(this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
+            }
+        },
+        moveItemDown(blockId, sectionIndex) {
+            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
+            const block = this.blockById({id: blockId});
+            if (this.currentSections[sectionIndex].blocks.length - 1 > currentIndex) {
+                const newPos = currentIndex + 1;
+                this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
+                this.assistiveLive =
+                    this.$gettextInterpolate(
+                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
+                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
+                    );
+            } else if (this.currentSections.length - 1 > sectionIndex) {
+                const newSectionIndex = sectionIndex + 1;
+                if (!this.sortInSlots.includes(newSectionIndex)) {
+                    this.sortInSlots.push(newSectionIndex);
+                }
+                this.currentSections[newSectionIndex].blocks.splice(0, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
+            }
+        },
+        abortKeyboardSorting(blockId, sectionIndex) {
+            const block = this.blockById({id: blockId});
+            this.keyboardSelected = null;
+            this.assistiveLive =
+                this.$gettextInterpolate(
+                    this.$gettext('%{blockTitle} Block, Neuordnung abgebrochen.')
+                    , {blockTitle: block.attributes.title}
+                );
+            this.initCurrentData();
+        },
+        storeKeyboardSorting(blockId, sectionIndex) {
+            const block = this.blockById({id: blockId});
+            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
+            this.keyboardSelected = null;
+            this.assistiveLive =
+                this.$gettextInterpolate(
+                    this.$gettext('%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.')
+                    , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
+                );
+            this.storeSort();
+        }
+    },
+    watch: {
+        blocks(newBlocks, oldBlocks) {
+            if (!this.showEdit && !this.checkSimpleArrayEquality(newBlocks, oldBlocks)) {
+                this.$nextTick(() => {
+                    setTimeout(() =>  this.initCurrentData(), 250);
+                });
+            }
+        },
+        currentSections: {
+            handler() {
+                if (this.keyboardSelected) {
+                    this.$nextTick(() => {
+                        this.$refs['sortableHandle' + this.keyboardSelected][0].focus();
+                    });
+                }
+            },
+            deep: true
+        }
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/containers/CoursewareBlockAdderArea.vue b/resources/vue/components/courseware/containers/CoursewareBlockAdderArea.vue
new file mode 100644
index 0000000..b81ee96
--- /dev/null
+++ b/resources/vue/components/courseware/containers/CoursewareBlockAdderArea.vue
@@ -0,0 +1,72 @@
+<template>
+    <button
+        class="cw-block-adder-area"
+        :class="{ 'cw-block-adder-active': adderActive }"
+        :aria-pressed="adderActive"
+        @click="selectBlockAdder"
+    >
+        <studip-icon v-show="!adderActive" shape="add" />
+        <studip-icon v-show="adderActive" shape="add" role="info_alt"/>
+        <span v-show="!adderActive"><translate>Block zu diesem Abschnitt hinzufügen</translate></span>
+        <span v-show="adderActive"><translate>Abschnitt aktiv - Blöcke werden hier eingefügt</translate></span>
+    </button>
+</template>
+
+<script>
+import StudipIcon from '../../StudipIcon.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+  components: { StudipIcon },
+    name: 'courseware-block-adder-area',
+    props: {
+        container: Object,
+        section: Number,
+    },
+    data() {
+        return {
+            adderActive: false,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            adderStorage: 'blockAdder',
+        }),
+        adderDisable() {
+            return Object.keys(this.adderStorage).length !== 0 && !this.adderActive;
+        },
+    },
+    methods: {
+        ...mapActions({
+            coursewareBlockAdder: 'coursewareBlockAdder',
+            coursewareSelectedToolbarItem: 'coursewareSelectedToolbarItem',
+            coursewareShowToolbar: 'coursewareShowToolbar'
+        }),
+        selectBlockAdder() {
+            if (this.adderActive) {
+                this.adderActive = false;
+                this.coursewareBlockAdder({});
+            } else {
+                this.adderActive = true;
+                this.coursewareBlockAdder({ container: this.container, section: this.section });
+                this.coursewareSelectedToolbarItem('blockadder');
+                this.coursewareShowToolbar(true);
+            }
+        },
+    },
+    watch: {
+        adderStorage(newValue, oldValue) {
+            if (Object.keys(newValue).length === 0) {
+                this.adderActive = false;
+                this.$emit('updateContainerContent', oldValue);
+            } else {
+                if (newValue.container && newValue.container.id === this.container.id && newValue.section === this.section) {
+                    this.adderActive = true;
+                } else {
+                    this.adderActive = false;
+                }
+            }
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
new file mode 100644
index 0000000..2480aba
--- /dev/null
+++ b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
@@ -0,0 +1,88 @@
+<template>
+    <div v-if="canEdit" class="cw-container-actions">
+        <studip-action-menu
+            :items="menuItems"
+            :context="container.attributes.title"
+            @editContainer="editContainer"
+            @changeContainer="changeContainer"
+            @deleteContainer="deleteContainer"
+            @removeLock="removeLock"
+            @copyToClipboard="copyToClipboard"
+        />
+    </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex';
+export default {
+    name: 'courseware-container-actions',
+    props: {
+        canEdit: Boolean,
+        container: Object,
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            userIsTeacher: 'userIsTeacher',
+        }),
+        blocked() {
+            return this.container?.relationships?.['edit-blocker']?.data !== null;
+        },
+        blockerId() {
+            return this.blocked ? this.container?.relationships?.['edit-blocker']?.data?.id : null;
+        },
+        blockedByThisUser() {
+            return this.blocked && this.userId === this.blockerId;
+        },
+        blockedByAnotherUser() {
+            return this.blocked && this.userId !== this.blockerId;
+        },
+        menuItems() {
+            let menuItems = [];
+            if (!this.blockedByAnotherUser) {
+                if (this.container.attributes["container-type"] !== 'list') {
+                    menuItems.push({ id: 1, label: this.$gettext('Abschnitt bearbeiten'), icon: 'edit', emit: 'editContainer' });
+                }
+                menuItems.push({ id: 2, label: this.$gettext('Abschnitt verändern'), icon: 'settings', emit: 'changeContainer' });
+                menuItems.push({ id: 3, label: this.$gettext('Abschnitt merken'), icon: 'clipboard', emit: 'copyToClipboard' });
+                menuItems.push({ id: 5, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' });
+            }
+
+            if (this.blocked && this.blockedByAnotherUser && this.userIsTeacher) {
+                menuItems.push({
+                    id: 3,
+                    label: this.$gettext('Sperre aufheben'),
+                    icon: 'lock-unlocked',
+                    emit: 'removeLock',
+                });
+            }
+
+            menuItems.sort((a, b) => {
+                return a.id > b.id ? 1 : b.id > a.id ? -1 : 0;
+            });
+            return menuItems;
+        },
+
+    },
+    methods: {
+        menuAction(action) {
+            this[action]();
+        },
+        editContainer() {
+            this.$emit('editContainer');
+        },
+        changeContainer() {
+            this.$emit('changeContainer');
+        },
+        deleteContainer() {
+            this.$emit('deleteContainer');
+        },
+        removeLock() {
+            this.$emit('removeLock');
+        },
+        copyToClipboard() {
+            this.$emit('copyToClipboard');
+        }
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/containers/CoursewareContainerAdderItem.vue b/resources/vue/components/courseware/containers/CoursewareContainerAdderItem.vue
new file mode 100644
index 0000000..78cc4ed
--- /dev/null
+++ b/resources/vue/components/courseware/containers/CoursewareContainerAdderItem.vue
@@ -0,0 +1,52 @@
+<template>
+    <a href="#" @click.prevent="addContainer">
+        <div class="cw-containeradder-item" :class="['cw-containeradder-item-' + type]">
+            <header class="cw-containeradder-item-title">
+                {{ title }}
+            </header>
+            <p class="cw-containeradder-item-description">
+                {{ description }}
+            </p>
+        </div>
+    </a>
+</template>
+<script>
+import { mapActions } from 'vuex';
+export default {
+    name: 'courseware-container-adder-item',
+    components: {},
+    props: {
+        title: String,
+        description: String,
+        type: String,
+        colspan: String,
+        firstSection: String,
+        secondSection: String,
+    },
+    methods: {
+        ...mapActions({
+            createContainer: 'createContainer',
+            companionSuccess: 'companionSuccess',
+        }),
+        async addContainer() {
+            let attributes = {};
+            attributes["container-type"] = this.type;
+            let sections = [];
+            if (this.type === 'list') {
+                sections = [{ name: this.firstSection, icon: '', blocks: [] }];
+            } else {
+                sections = [{ name: this.firstSection, icon: '', blocks: [] },{ name: this.secondSection, icon: '', blocks: [] }];
+            }
+            attributes.payload = {
+                colspan: this.colspan,
+                sections: sections,
+            };
+            await this.createContainer({ structuralElementId: this.$route.params.id, attributes: attributes });
+            this.companionSuccess({
+                info: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'),
+            });
+        },
+    },
+    mounted() {},
+};
+</script>
diff --git a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
new file mode 100644
index 0000000..e8e2bb5
--- /dev/null
+++ b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
@@ -0,0 +1,392 @@
+<template>
+    <div
+        class="cw-container cw-container-list"
+        :class="['cw-container-colspan-' + colSpan, showEditMode && canEdit ? 'cw-container-active' : '']"
+    >
+        <div class="cw-container-content">
+            <header v-if="showEditMode && canEdit" class="cw-container-header" :class="{ 'cw-container-header-open': isOpen }">
+                <a href="#" class="cw-container-header-toggle" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen">
+                    <studip-icon :shape="isOpen ? 'arr_1down' : 'arr_1right'" />
+                    <span>{{ container.attributes.title }} ({{container.attributes.width}})</span>
+                    <studip-icon v-if="blockedByAnotherUser" shape="lock-locked" />
+                    <span v-if="blockedByAnotherUser" class="cw-default-container-blocker-warning">
+                        {{ $gettextInterpolate($gettext('Wird im Moment von %{ userName } bearbeitet'), { userName: this.blockingUserName }) }}
+                    </span>
+                </a>
+                <courseware-container-actions
+                    :canEdit="canEdit"
+                    :container="container"
+                    @editContainer="displayEditDialog"
+                    @changeContainer="displayChangeDialog"
+                    @deleteContainer="displayDeleteDialog"
+                    @removeLock="displayRemoveLockDialog"
+                    @copyToClipboard="copyToClipboard"
+                />
+            </header>
+            <div v-show="isOpen"
+                class="cw-block-wrapper"
+                :class="{
+                    'cw-block-wrapper-active': showEditMode,
+                }"
+            >
+                <slot name="containerContent"></slot>
+            </div>
+
+            <studip-dialog
+                v-if="showEditDialog"
+                :title="textEditTitle"
+                :confirmText="textEditConfirm"
+                confirmClass="accept"
+                :closeText="textEditClose"
+                closeClass="cancel"
+                @close="closeEdit"
+                @confirm="storeContainer"
+                height="430"
+                width="680"
+            >
+                <template v-slot:dialogContent>
+                    <slot name="containerEditDialog"></slot>
+                </template>
+            </studip-dialog>
+
+            <studip-dialog
+                v-if="showChangeDialog"
+                :title="$gettext('Abschnitt verändern')"
+                :confirmText="$gettext('Speichern')"
+                confirmClass="accept"
+                :closeText="$gettext('Abbrechen')"
+                closeClass="cancel"
+                @close="closeChange"
+                @confirm="storeChange"
+                height="520"
+                width="480"
+            >
+                <template v-slot:dialogContent>
+                    <form class="default" @submit.prevent="">
+                        <div class="cw-radioset-wrapper" role="group" aria-labelledby="container-type">
+                            <p id="container-type">{{ $gettext('Typ') }}</p>
+                            <div class="cw-radioset">
+                                <div 
+                                    v-for="(container, index) in containerTypes"
+                                    :key="index"
+                                    class="cw-radioset-box" 
+                                    :class="[container.type === changeType ? 'selected' : '']"
+                                >
+                                    <input type="radio" :id="'type-' + container.type" :value="container.type" v-model="changeType" name="container-type"/>    
+                                    <label :for="'type-' + container.type" >
+                                        <div class="label-icon" :class="[container.type, container.type === changeType ? 'selected' : '']"></div>
+                                        <p>{{ container.title }}</p>
+                                    </label>
+                                    
+                                </div>
+                            </div>
+                        </div>
+                        <div class="cw-radioset-wrapper" role="group" aria-labelledby="container-style">
+                            <p id="container-style">{{ $gettext('Stil') }}</p>
+                            <div class="cw-radioset">
+                                <div
+                                v-for="(style, index) in containerStyles"
+                                :key="index"
+                                class="cw-radioset-box"
+                                :class="[style.colspan === changeStyle ? 'selected' : '']"
+                                >
+                                    <input type="radio" :id="'change-style-' + style.colspan" :value="style.colspan" v-model="changeStyle" name="change-container-style"/>
+                                    <label :for="'change-style-' + style.colspan">
+                                        <div class="label-icon" :class="[style.colspan, style.colspan === changeStyle ? 'selected' : '']"></div>
+                                        <p>{{ style.title }}</p>
+                                    </label>
+                                </div>
+                            </div>
+                        </div>
+                    </form>
+                </template>
+            </studip-dialog>
+
+
+            <studip-dialog
+                v-if="showDeleteDialog"
+                :title="textDeleteTitle"
+                :question="textDeleteAlert"
+                height="180"
+                width="380"
+                @confirm="executeDelete"
+                @close="closeDeleteDialog"
+            ></studip-dialog>
+
+            <studip-dialog
+                v-if="showRemoveLockDialog"
+                :title="textRemoveLockTitle"
+                :question="textRemoveLockAlert"
+                height="200"
+                width="450"
+                @confirm="executeRemoveLock"
+                @close="showRemoveLockDialog = false"
+            ></studip-dialog>
+
+        </div>
+    </div>
+</template>
+
+<script>
+import CoursewareContainerActions from './CoursewareContainerActions.vue';
+import StudipDialog from '../../StudipDialog.vue';
+import { mapGetters, mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-default-container',
+    components: {
+        CoursewareContainerActions,
+        StudipDialog,
+    },
+    props: {
+        containerClass: String,
+        container: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            showDeleteDialog: false,
+            showEditDialog: false,
+            showChangeDialog: false,
+            showRemoveLockDialog: false,
+            textEditConfirm: this.$gettext('Speichern'),
+            textEditClose: this.$gettext('Schließen'),
+            textEditTitle: this.$gettext('Abschnitt bearbeiten'),
+            textDeleteTitle: this.$gettext('Abschnitt unwiderruflich löschen'),
+            textDeleteAlert: this.$gettext('Möchten Sie diesen Abschnitt wirklich löschen?'),
+            textRemoveLockTitle: this.$gettext('Sperre aufheben'),
+            textRemoveLockAlert: this.$gettext('Möchten Sie die Sperre dieses Abschnitts wirklich aufheben?'),
+            isOpen: true,
+
+            changeType: '',
+            changeStyle: '',
+        };
+    },
+    computed: {
+        ...mapGetters({
+            blockAdder: 'blockAdder',
+            userId: 'userId',
+            userById: 'users/byId',
+            viewMode: 'viewMode',
+            currentElementisLink: 'currentElementisLink',
+            containerTypes: 'containerTypes',
+        }),
+        showEditMode() {
+            return this.viewMode === 'edit' && !this.currentElementisLink;
+        },
+        colSpan() {
+            return this.container.attributes.payload.colspan ? this.container.attributes.payload.colspan : 'full';
+        },
+        blocked() {
+            return this.container?.relationships?.['edit-blocker']?.data !== null;
+        },
+        blockerId() {
+            return this.blocked ? this.container?.relationships?.['edit-blocker']?.data?.id : null;
+        },
+        blockedByThisUser() {
+            return this.blocked && this.userId === this.blockerId;
+        },
+        blockedByAnotherUser() {
+            return this.blocked && this.userId !== this.blockerId;
+        },
+        blockingUser() {
+            if (this.blockedByAnotherUser) {
+                return this.userById({id: this.blockerId});
+            }
+
+            return null;
+        },
+        blockingUserName() {
+            return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : '';
+        },
+        containerStyles() {
+            return [
+                { title: this.$gettext('Volle Breite'), colspan: 'full'},
+                { title: this.$gettext('Halbe Breite'), colspan: 'half' },
+                { title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' },
+            ];
+        },
+        type() {
+            return this.container.attributes['container-type'];
+        }
+    },
+    methods: {
+        ...mapActions({
+            companionInfo: 'companionInfo',
+            companionWarning: 'companionWarning',
+            companionSuccess: 'companionSuccess',
+            updateContainer: 'updateContainer',
+            loadContainer: 'courseware-containers/loadById',
+            deleteContainer: 'deleteContainer',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+            coursewareBlockAdder: 'coursewareBlockAdder',
+            createClipboard: 'courseware-clipboards/create'
+        }),
+        async displayEditDialog() {
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
+
+                return false;
+            }
+
+            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
+            this.showEditDialog = true;
+        },
+        async displayChangeDialog() {
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
+
+                return false;
+            }
+
+            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
+            this.changeType = this.type;
+            this.changeStyle = this.colSpan;
+            this.showChangeDialog = true;
+        },
+        async storeChange() {
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+            this.closeChange();
+            if (this.blockedByAnotherUser) {
+                this.companionWarning({
+                    info: this.$gettextInterpolate(
+                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
+                        {blockingUserName: this.blockingUserName}
+                    )
+                });
+                return;
+            }
+            if (this.blockerId === null) {
+                await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
+            }
+
+            let container = this.container;
+            container.attributes['container-type'] = this.changeType;
+            container.attributes.payload.colspan = this.changeStyle;
+            await this.updateContainer({
+                container: container,
+                structuralElementId: this.container.relationships['structural-element'].data.id,
+            });
+            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            await this.loadContainer({id : this.container.id });
+        },
+        async closeChange() {
+            await this.loadContainer({ id: this.container.id });
+            this.showChangeDialog = false;
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            }
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+        },
+        async closeEdit() {
+            await this.loadContainer({ id: this.container.id });
+            this.$emit('closeEdit');
+            this.showEditDialog = false;
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            }
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+        },
+        async storeContainer() {
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+            if (this.blockedByThisUser) {
+                this.$emit('storeContainer');
+            }
+            if (this.blockedByAnotherUser) {
+                this.companionWarning({
+                    info: this.$gettextInterpolate(
+                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
+                        {blockingUserName: this.blockingUserName}
+                    )
+                });
+                this.$emit('closeEdit');
+            }
+            if (this.blockerId === null) {
+                await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
+                this.$emit('storeContainer');
+            }
+            this.showEditDialog = false;
+        },
+        async displayDeleteDialog() {
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+            if (!this.blocked) {
+                await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
+                this.showDeleteDialog = true;
+            } else {
+                if (this.blockedByThisUser) {
+                    this.showDeleteDialog = true;
+                } else {
+                    this.companionInfo({
+                        info: this.$gettextInterpolate(
+                            this.$gettext('Löschen nicht möglich, da %{blockingUserName} den Abschnitt bearbeitet.'),
+                            {blockingUserName: this.blockingUserName}
+                        )
+                    });
+                }
+            }
+        },
+        async closeDeleteDialog() {
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            }
+            this.showDeleteDialog = false;
+        },
+        async executeDelete() {
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({
+                    info: this.$gettextInterpolate(
+                        this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'),
+                        {blockingUserName: this.blockingUserName}
+                    )
+                });
+                return false;
+            }
+            await this.deleteContainer({
+                containerId: this.container.id,
+                structuralElementId: this.container.relationships['structural-element'].data.id,
+            });
+            if(Object.keys(this.blockAdder).length !== 0 && this.blockAdder.container.id === this.container.id) {
+                this.coursewareBlockAdder({});
+            }
+            this.showDeleteDialog = false;
+        },
+        displayRemoveLockDialog() {
+            this.showRemoveLockDialog = true;
+        },
+        async executeRemoveLock() {
+            await this.unlockObject({ id: this.container.id , type: 'courseware-containers' });
+            await this.loadContainer({ id: this.container.id });
+            this.showRemoveLockDialog = false;
+        },
+
+        async copyToClipboard() {
+            const clipboard = {
+                attributes: {
+                    name: this.container.attributes.title,
+                    'container-id': this.container.id,
+                    'object-type': this.container.type,
+                    'object-kind': this.container.attributes['container-type'],
+                    
+                }
+            };
+
+            await this.createClipboard(clipboard, { root: true });
+            this.companionSuccess({ info: this.$gettext('Abschnitt wurde in Merkliste abgelegt.') });
+        }
+
+    },
+
+    watch: {
+        showEditDialog(state) {
+            this.$emit('showEdit', state);
+        }
+    }
+
+};
+</script>
diff --git a/resources/vue/components/courseware/containers/CoursewareListContainer.vue b/resources/vue/components/courseware/containers/CoursewareListContainer.vue
new file mode 100644
index 0000000..6d8089f
--- /dev/null
+++ b/resources/vue/components/courseware/containers/CoursewareListContainer.vue
@@ -0,0 +1,285 @@
+<template>
+    <courseware-default-container
+        :container="container"
+        containerClass="cw-container-list"
+        :canEdit="canEdit"
+        :isTeacher="isTeacher"
+        @storeContainer="storeContainer"
+    >
+        <template v-slot:containerContent>
+            <ul v-if="!showEditMode || currentElementisLink"  class="cw-container-list-block-list">
+                <li v-for="block in blocks" :key="block.id" class="cw-block-item">
+                    <component :is="component(block)" :block="block" :canEdit="canEdit" :isTeacher="isTeacher" />
+                </li>
+            </ul>
+            <template v-else>
+                <template v-if="!processing">
+                    <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
+                    <span id="operation" class="assistive-text">
+                        {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
+                    </span>
+                    <draggable
+                        v-if="showEditMode && canEdit"
+                        class="cw-container-list-block-list cw-container-list-sort-mode"
+                        tag="ol"
+                        role="listbox"
+                        v-model="blockList"
+                        v-bind="dragOptions"
+                        handle=".cw-sortable-handle"
+                        group="blocks"
+                        @start="isDragging = true"
+                        @end="dropBlock"
+                        ref="sortables"
+                        :containerId="container.id"
+                        sectionId="0"
+                    >
+                        <li
+                            v-for="block in blockList"
+                            :key="block.id"
+                            class="cw-block-item cw-block-item-sortable"
+                        >
+                            <span
+                                :class="{ 'cw-sortable-handle-dragging': isDragging }"
+                                class="cw-sortable-handle"
+                                tabindex="0"
+                                role="button"
+                                aria-describedby="operation"
+                                :ref="'sortableHandle' + block.id"
+                                @keydown="keyHandler($event, block.id)"
+                            ></span>
+                            <component
+                                :is="component(block)"
+                                :block="block"
+                                :canEdit="canEdit"
+                                :isTeacher="isTeacher"
+                                :class="{ 'cw-block-item-selected': keyboardSelected === block.id}"
+                                :blockId="block.id"
+                            />
+                        </li>
+                    </draggable>
+                    <courseware-block-adder-area :container="container" :section="0" />
+                </template>
+                <div v-else class="progress-wrapper" :style="{ height: contentHeight + 'px' }">
+                    <studip-progress-indicator :description="$gettext('Vorgang wird bearbeitet...')" />
+                </div>
+            </template>
+        </template>
+    </courseware-default-container>
+</template>
+
+<script>
+import ContainerComponents from './container-components.js';
+import containerMixin from '@/vue/mixins/courseware/container.js';
+import draggable from 'vuedraggable';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-list-container',
+    mixins: [containerMixin],
+    components: Object.assign(ContainerComponents, {
+        draggable,
+    }),
+    props: {
+        container: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+        canAddElements: Boolean,
+    },
+    data() {
+        return {
+            isDragging: false,
+            dragOptions: {
+                animation: 0,
+                group: this.container.id,
+                disabled: false,
+                ghostClass: "block-ghost"
+            },
+            blockList: [],
+            processing: false,
+            contentHeight: 0,
+            keyboardSelected: null,
+            assistiveLive: ''
+        };
+    },
+    computed: {
+        ...mapGetters({
+            blockById: 'courseware-blocks/byId',
+            containerById: 'courseware-containers/byId',
+            viewMode: 'viewMode',
+            currentElementisLink: 'currentElementisLink'
+        }),
+        blocked() {
+            return this.container?.relationships?.['edit-blocker']?.data !== null;
+        },
+        blockerId() {
+            return this.blocked ? this.container?.relationships?.['edit-blocker']?.data?.id : null;
+        },
+        blockedByThisUser() {
+            return this.blocked && this.userId === this.blockerId;
+        },
+        blockedByAnotherUser() {
+            return this.blocked && this.userId !== this.blockerId;
+        },
+        showEditMode() {
+            return this.viewMode === 'edit';
+        },
+        blocks() {
+            if (!this.container) {
+                return [];
+            }
+            let containerBlocks = this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })).filter(Boolean);
+            let unallocated = new Set(containerBlocks.map(({ id }) => id));
+            let sortedBlocks = this.container.attributes.payload.sections[0].blocks.map((id) => this.blockById({ id })).filter(Boolean);
+            sortedBlocks.forEach(({ id }) => unallocated.delete(id));
+            let unallocatedBlocks = [...unallocated].map((id) => this.blockById({ id }));
+
+            return sortedBlocks.concat(unallocatedBlocks);
+        },
+    },
+    methods: {
+        ...mapActions({
+            updateContainer: 'updateContainer',
+            loadContainer: 'courseware-containers/loadById',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+            companionInfo: 'companionInfo'
+        }),
+        storeContainer(data) {
+        },
+        initCurrentData() {
+            this.blockList = this.blocks;
+        },
+        async storeSort() {
+            this.contentHeight = this.$refs.sortables.$el.offsetHeight;
+            const timeout = setTimeout(() => this.processing = true, 800);
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
+                clearTimeout(timeout);
+                this.processing = false;
+                this.loadContainer({id : this.container.id });
+                return false;
+            }
+            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
+            let currentContainer = this.container;
+            currentContainer.attributes.payload.sections[0].blocks = this.blockList.map(block => {return block.id});
+            await this.updateContainer({
+                container: currentContainer,
+                structuralElementId: currentContainer.relationships['structural-element'].data.id,
+            });
+            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            await this.loadContainer({id : this.container.id });
+            this.initCurrentData();
+            clearTimeout(timeout);
+            this.processing = false;
+            this.assistiveLive = '';
+        },
+        component(block) {
+            if (block.attributes["block-type"] !== undefined) {
+                return 'courseware-' + block.attributes["block-type"] + '-block';
+            }
+            return null;
+        },
+        keyHandler(e, blockId) {
+            switch (e.keyCode) {
+                case 27: // esc
+                    this.abortKeyboardSorting(blockId);
+                    break;
+                case 13: // enter
+                    e.preventDefault();
+                    if (this.keyboardSelected) {
+                        this.storeKeyboardSorting(blockId);
+                    } else {
+                        this.keyboardSelected = blockId;
+                        const block = this.blockById({id: blockId});
+                        const index = this.blockList.findIndex(b => b.id === block.id);
+                        this.assistiveLive = 
+                            this.$gettextInterpolate(
+                                this.$gettext('%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
+                                , {blockTitle: block.attributes.title, pos: index + 1, listLength: this.blockList.length}
+                            );
+                    }
+                    break;
+            }
+            if (this.keyboardSelected) {
+                switch (e.keyCode) {
+                    case 9: //tab
+                        this.abortKeyboardSorting(blockId);
+                        break;
+                    case 38: // up
+                        e.preventDefault();
+                        this.moveItemUp(blockId);
+                        break;
+                    case 40: // down
+                        e.preventDefault();
+                        this.moveItemDown(blockId);
+                        break;
+                }
+            }
+        },
+        moveItemUp(blockId) {
+            const currentIndex = this.blockList.findIndex(block => block.id === blockId);
+            if (currentIndex !== 0) {
+                const block = this.blockById({id: blockId});
+                const newPos = currentIndex - 1;
+                this.blockList.splice(newPos, 0, this.blockList.splice(currentIndex, 1)[0]);
+                this.assistiveLive = 
+                    this.$gettextInterpolate(
+                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
+                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.blockList.length}
+                    );
+            }
+        },
+        moveItemDown(blockId) {
+            const currentIndex = this.blockList.findIndex(block => block.id === blockId);
+            if (this.blockList.length - 1 > currentIndex) {
+                const block = this.blockById({id: blockId});
+                const newPos = currentIndex + 1;
+                this.blockList.splice(newPos, 0, this.blockList.splice(currentIndex, 1)[0]);
+                this.assistiveLive = 
+                    this.$gettextInterpolate(
+                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
+                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.blockList.length}
+                    );
+            }
+        },
+        abortKeyboardSorting(blockId) {
+            const block = this.blockById({id: blockId});
+            this.keyboardSelected = null;
+            this.assistiveLive = 
+                this.$gettextInterpolate(
+                    this.$gettext('%{blockTitle} Block, Neuordnung abgebrochen.')
+                    , {blockTitle: block.attributes.title}
+                );
+            this.initCurrentData();
+        },
+        storeKeyboardSorting(blockId) {
+            const block = this.blockById({id: blockId});
+            const currentIndex = this.blockList.findIndex(block => block.id === blockId);
+            this.keyboardSelected = null;
+            this.assistiveLive = 
+                this.$gettextInterpolate(
+                    this.$gettext('%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.')
+                    , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.blockList.length}
+                );
+            this.storeSort();
+        }
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    watch: {
+        blocks() {
+            this.initCurrentData();
+        },
+        blockList() {
+            if (this.keyboardSelected) {
+                this.$nextTick(() => {
+                    const selected = this.$refs['sortableHandle' + this.keyboardSelected][0];
+                    selected.focus();
+                    selected.scrollIntoView({behavior: "smooth", block: "center"});
+                });
+            }
+        }
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue b/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue
new file mode 100644
index 0000000..c6264c7
--- /dev/null
+++ b/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue
@@ -0,0 +1,399 @@
+<template>
+    <courseware-default-container
+        :container="container"
+        containerClass="cw-container-tabs"
+        :canEdit="canEdit"
+        :isTeacher="isTeacher"
+        @showEdit="setShowEdit"
+        @storeContainer="storeContainer"
+        @closeEdit="initCurrentData"
+    >
+        <template v-slot:containerContent>
+            <template v-if="showEditMode && canEdit && !currentElementisLink">
+                <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
+                <span id="operation" class="assistive-text">
+                    {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
+                </span>
+            </template>
+            <courseware-tabs>
+                <courseware-tab
+                    v-for="(section, index) in currentSections"
+                    :key="index"
+                    :index="index"
+                    :name="section.name"
+                    :icon="section.icon"
+                    :selected="sortInTab === index"
+                >
+                    <ul v-if="!showEditMode || currentElementisLink" class="cw-container-tabs-block-list">
+                        <li v-for="block in section.blocks" :key="block.id" class="cw-block-item">
+                            <component
+                                :is="component(block)"
+                                :block="block"
+                                :canEdit="canEdit"
+                                :isTeacher="isTeacher"
+                            />
+                        </li>
+                    </ul>
+                    <template v-else>
+                        <template v-if="canEdit">
+                            <draggable
+                                class="cw-container-list-block-list cw-container-list-sort-mode"
+                                :class="[section.blocks.length === 0 ? 'cw-container-list-sort-mode-empty' : '']"
+                                tag="ol"
+                                role="listbox"
+                                v-model="section.blocks"
+                                v-bind="dragOptions"
+                                handle=".cw-sortable-handle"
+                                group="blocks"
+                                @start="isDragging = true"
+                                @end="dropBlock"
+                                :containerId="container.id"
+                                :sectionId="index"
+                            >
+                                <li v-for="block in section.blocks" :key="block.id" class="cw-block-item cw-block-item-sortable">
+                                    <span
+                                        :class="{ 'cw-sortable-handle-dragging': isDragging }"
+                                        class="cw-sortable-handle"
+                                        tabindex="0"
+                                        role="button"
+                                        aria-describedby="operation"
+                                        :ref="'sortableHandle' + block.id"
+                                        @keydown="keyHandler($event, block.id, index)"
+                                    ></span>
+                                    <component
+                                        :is="component(block)"
+                                        :block="block"
+                                        :canEdit="canEdit"
+                                        :isTeacher="isTeacher"
+                                        :class="{ 'cw-block-item-selected': keyboardSelected === block.id}"
+                                        :blockId="block.id"
+                                    />
+                                </li>
+                            </draggable>
+                            <template v-if="canAddElements">
+                                <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/>
+                            </template>
+                        </template>
+                    </template>
+                </courseware-tab>
+            </courseware-tabs>
+        </template>
+        <template v-slot:containerEditDialog>
+            <form class="default cw-container-dialog-edit-form" @submit.prevent="">
+                <fieldset v-for="(section, index) in currentContainer.attributes.payload.sections.filter(section => !section.locked)" :key="index">
+                    <label>
+                        <translate>Title</translate>
+                        <input type="text" v-model="section.name" />
+                    </label>
+                    <label>
+                        <translate>Icon</translate>
+                        <studip-select :options="icons" v-model="section.icon">
+                            <template #open-indicator="selectAttributes">
+                                <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
+                            </template>
+                            <template #no-options>
+                                <translate>Es steht keine Auswahl zur Verfügung.</translate>
+                            </template>
+                            <template #selected-option="option">
+                                <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
+                            </template>
+                            <template #option="option">
+                                <studip-icon :shape="option.label"/> <span class="vs__option-with-icon">{{option.label}}</span>
+                            </template>
+                        </studip-select>
+                    </label>
+                    <label
+                        class="cw-container-section-delete"
+                        v-if="currentContainer.attributes.payload.sections.length > 1"
+                    >
+                    <button class="button trash" @click="deleteSection(index)"><translate>Tab löschen</translate></button>
+                    </label>
+                </fieldset>
+            </form>
+            <button class="button add" @click="addSection"><translate>Tab hinzufügen</translate></button>
+        </template>
+    </courseware-default-container>
+</template>
+
+<script>
+import ContainerComponents from './container-components.js';
+import CoursewareTabs from '../layouts/CoursewareTabs.vue';
+import CoursewareTab from '../layouts/CoursewareTab.vue';
+import containerMixin from '@/vue/mixins/courseware/container.js';
+import contentIconsMixin from '@/vue/mixins/courseware/content-icons.js';
+
+import { mapGetters, mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-tabs-container',
+    mixins: [containerMixin, contentIconsMixin],
+    components: Object.assign(ContainerComponents, {
+        CoursewareTabs,
+        CoursewareTab,
+    }),
+    props: {
+        container: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+        canAddElements: Boolean,
+    },
+    data() {
+        return {
+            showEdit: false,
+            currentContainer: null,
+            currentSections: [],
+            unallocatedBlocks: [],
+            textDeleteSection: this.$gettext('Sektion entfernen'),
+            selectAttributes: {'ref': 'openIndicator', 'role': 'presentation', 'class': 'vs__open-indicator'},
+            sortMode: false,
+            isDragging: false,
+            dragOptions: {
+                animation: 0,
+                group: this.container.id,
+                disabled: false,
+                ghostClass: "block-ghost"
+            },
+            processing: false,
+            keyboardSelected: null,
+            sortInTab: 0,
+            assistiveLive: ''
+        };
+    },
+    computed: {
+        ...mapGetters({
+            blockById: 'courseware-blocks/byId',
+            viewMode: 'viewMode',
+            currentElementisLink: 'currentElementisLink'
+        }),
+        showEditMode() {
+            return this.viewMode === 'edit';
+        },
+        blocks() {
+            if (!this.container) {
+                return [];
+            }
+
+            return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })).filter((a) => a);
+        },
+        icons() {
+            return this.contentIcons;
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    methods: {
+        ...mapActions({
+            updateContainer: 'updateContainer',
+            loadContainer: 'courseware-containers/loadById',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+        }),
+        initCurrentData() {
+            this.currentContainer = _.cloneDeep(this.container);
+
+            let view = this;
+            let sections = this.currentContainer.attributes.payload.sections;
+
+            const unallocated = new Set(this.blocks.map(({ id }) => id));
+
+            for (let section of sections) {
+                section.locked = false;
+                section.blocks = section.blocks.map((id) =>  view.blockById({id})).filter(Boolean);
+                for (let sectionBlock of section.blocks) {
+                    if (sectionBlock?.id && unallocated.has(sectionBlock.id)) {
+                        unallocated.delete(sectionBlock.id);
+                    }
+                }
+            }
+
+            if (unallocated.size > 0) {
+                this.unallocatedBlocks = [...unallocated].map((id) => view.blockById({ id }));
+                sections.push({
+                    blocks: this.unallocatedBlocks,
+                    name: this.$gettext('nicht zugewiesene Inhalte'),
+                    icon: 'decline',
+                    locked: true
+                });
+            }
+
+            this.currentSections = sections;
+        },
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
+        addSection() {
+            this.currentContainer.attributes.payload.sections.push({ name: '', icon: '', blocks: [] });
+        },
+        deleteSection(index) {
+            if (this.currentContainer.attributes.payload.sections.length === 1) {
+                return;
+            }
+            if (this.currentContainer.attributes.payload.sections[index].blocks.length > 0) {
+                if (index === 0) {
+                    this.currentContainer.attributes.payload.sections[
+                        index + 1
+                    ].blocks = this.currentContainer.attributes.payload.sections[index + 1].blocks.concat(
+                        this.currentContainer.attributes.payload.sections[index].blocks
+                    );
+                } else {
+                    this.currentContainer.attributes.payload.sections[
+                        index - 1
+                    ].blocks = this.currentContainer.attributes.payload.sections[index - 1].blocks.concat(
+                        this.currentContainer.attributes.payload.sections[index].blocks
+                    );
+                }
+            }
+            this.currentContainer.attributes.payload.sections.splice(index, 1);
+        },
+        async storeContainer() {
+            const timeout = setTimeout(() => this.processing = true, 800);
+            this.currentContainer.attributes.payload.sections = this.currentContainer.attributes.payload.sections.filter(section => !section.locked);
+            this.currentContainer.attributes.payload.sections.forEach(section => {
+                section.blocks = section.blocks.map((block) => {return block.id;});
+                delete section.locked;
+            });
+            await this.updateContainer({
+                container: this.currentContainer,
+                structuralElementId: this.currentContainer.relationships['structural-element'].data.id,
+            });
+            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            await this.loadContainer({id : this.container.id });
+            this.initCurrentData();
+            clearTimeout(timeout);
+            this.processing = false;
+        },
+        async storeSort() {
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
+                this.loadContainer({id : this.container.id });
+                return false;
+            }
+            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
+            this.storeContainer();
+        },
+        component(block) {
+            if (block.attributes) {
+                return 'courseware-' + block.attributes["block-type"] + '-block';
+            }
+            return null;
+        },
+        updateContent(blockAdder) {
+            if(blockAdder.container !== undefined && blockAdder.container.id === this.container.id) {
+                this.initCurrentData();
+            }
+        },
+        keyHandler(e, blockId, sectionIndex) {
+            switch (e.keyCode) {
+                case 27: // esc
+                    this.abortKeyboardSorting(blockId, sectionIndex);
+                    break;
+                case 13: // enter
+                    e.preventDefault();
+                    if (this.keyboardSelected) {
+                        this.storeKeyboardSorting(blockId, sectionIndex);
+                    } else {
+                        this.keyboardSelected = blockId;
+                        const block = this.blockById({id: blockId});
+                        const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
+                        this.assistiveLive =
+                            this.$gettextInterpolate(
+                                this.$gettext('%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
+                                , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
+                            );
+                    }
+                    break;
+            }
+            if (this.keyboardSelected) {
+                switch (e.keyCode) {
+                    case 9: //tab
+                        this.abortKeyboardSorting(blockId, sectionIndex);
+                        break;
+                    case 38: // up
+                        e.preventDefault();
+                        this.moveItemUp(blockId, sectionIndex);
+                        break;
+                    case 40: // down
+                        e.preventDefault();
+                        this.moveItemDown(blockId, sectionIndex);
+                        break;
+                }
+            }
+        },
+        moveItemUp(blockId, sectionIndex) {
+            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
+            const block = this.blockById({id: blockId});
+            if (currentIndex !== 0) {
+                const newPos = currentIndex - 1;
+                this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
+                this.assistiveLive =
+                    this.$gettextInterpolate(
+                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
+                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
+                    );
+            } else if (sectionIndex !== 0) {
+                const newSectionIndex = sectionIndex - 1;
+                this.sortInTab = newSectionIndex;
+                this.currentSections[newSectionIndex].blocks.push(this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
+            }
+        },
+        moveItemDown(blockId, sectionIndex) {
+            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
+            const block = this.blockById({id: blockId});
+            if (this.currentSections[sectionIndex].blocks.length - 1 > currentIndex) {
+                const newPos = currentIndex + 1;
+                this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
+                this.assistiveLive =
+                    this.$gettextInterpolate(
+                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
+                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
+                    );
+            } else if (this.currentSections.length - 1 > sectionIndex) {
+                const newSectionIndex = sectionIndex + 1;
+                this.sortInTab = newSectionIndex;
+                this.currentSections[newSectionIndex].blocks.splice(0, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
+            }
+        },
+        abortKeyboardSorting(blockId, sectionIndex) {
+            const block = this.blockById({id: blockId});
+            this.keyboardSelected = null;
+            this.assistiveLive =
+                this.$gettextInterpolate(
+                    this.$gettext('%{blockTitle} Block, Neuordnung abgebrochen.')
+                    , {blockTitle: block.attributes.title}
+                );
+            this.initCurrentData();
+        },
+        storeKeyboardSorting(blockId, sectionIndex) {
+            const block = this.blockById({id: blockId});
+            const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
+            this.keyboardSelected = null;
+            this.assistiveLive =
+                this.$gettextInterpolate(
+                    this.$gettext('%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.')
+                    , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
+                );
+            this.storeSort();
+        }
+    },
+    watch: {
+        blocks(newBlocks, oldBlocks) {
+            if (!this.showEdit && !this.checkSimpleArrayEquality(newBlocks, oldBlocks)) {
+                this.$nextTick(() => {
+                    setTimeout(() =>  this.initCurrentData(), 250);
+                });
+            }
+        },
+        currentSections: {
+            handler() {
+                if (this.keyboardSelected) {
+                    this.$nextTick(() => {
+                        this.$refs['sortableHandle' + this.keyboardSelected][0].focus();
+                    });
+                }
+            },
+            deep: true
+        }
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/containers/container-components.js b/resources/vue/components/courseware/containers/container-components.js
new file mode 100644
index 0000000..03c98b4
--- /dev/null
+++ b/resources/vue/components/courseware/containers/container-components.js
@@ -0,0 +1,77 @@
+import CoursewareDefaultBlock from '../blocks/CoursewareDefaultBlock.vue';
+import CoursewareDefaultContainer from './CoursewareDefaultContainer.vue';
+import CoursewareBlockAdderArea from './CoursewareBlockAdderArea.vue';
+// blocks
+import CoursewareAudioBlock from '../blocks/CoursewareAudioBlock.vue';
+import CoursewareBeforeAfterBlock from '../blocks/CoursewareBeforeAfterBlock.vue';
+import CoursewareBiographyAchievementsBlock from '../blocks/CoursewareBiographyAchievementsBlock.vue';
+import CoursewareBiographyCareerBlock from '../blocks/CoursewareBiographyCareerBlock.vue';
+import CoursewareBiographyGoalsBlock from '../blocks/CoursewareBiographyGoalsBlock.vue';
+import CoursewareBiographyPersonalInformationBlock from '../blocks/CoursewareBiographyPersonalInformationBlock.vue';
+import CoursewareCanvasBlock from '../blocks/CoursewareCanvasBlock.vue';
+import CoursewareChartBlock from '../blocks/CoursewareChartBlock.vue';
+import CoursewareCodeBlock from '../blocks/CoursewareCodeBlock.vue';
+import CoursewareConfirmBlock from '../blocks/CoursewareConfirmBlock.vue';
+import CoursewareDateBlock from '../blocks/CoursewareDateBlock.vue';
+import CoursewareDialogCardsBlock from '../blocks/CoursewareDialogCardsBlock.vue';
+import CoursewareDocumentBlock from '../blocks/CoursewareDocumentBlock.vue';
+import CoursewareDownloadBlock from '../blocks/CoursewareDownloadBlock.vue';
+import CoursewareEmbedBlock from '../blocks/CoursewareEmbedBlock.vue';
+import CoursewareErrorBlock from '../blocks/CoursewareErrorBlock.vue';
+import CoursewareFolderBlock from '../blocks/CoursewareFolderBlock.vue';
+import CoursewareGalleryBlock from '../blocks/CoursewareGalleryBlock.vue';
+import CoursewareHeadlineBlock from '../blocks/CoursewareHeadlineBlock.vue';
+import CoursewareIframeBlock from '../blocks/CoursewareIframeBlock.vue';
+import CoursewareImageMapBlock from '../blocks/CoursewareImageMapBlock.vue';
+import CoursewareKeyPointBlock from '../blocks/CoursewareKeyPointBlock.vue';
+import CoursewareLinkBlock from '../blocks/CoursewareLinkBlock.vue';
+import CoursewareLtiBlock from '../blocks/CoursewareLtiBlock.vue';
+import CoursewareTableOfContentsBlock from '../blocks/CoursewareTableOfContentsBlock.vue';
+import CoursewareTextBlock from '../blocks/CoursewareTextBlock.vue';
+import CoursewareTimelineBlock from '../blocks/CoursewareTimelineBlock.vue';
+import CoursewareTypewriterBlock from '../blocks/CoursewareTypewriterBlock.vue';
+import CoursewareVideoBlock from '../blocks/CoursewareVideoBlock.vue';
+//layout
+import StudipIcon from '../../StudipIcon.vue';
+import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
+
+const ContainerComponents = {
+    CoursewareDefaultBlock,
+    CoursewareDefaultContainer,
+    CoursewareBlockAdderArea,
+    // blocks
+    CoursewareAudioBlock,
+    CoursewareBeforeAfterBlock,
+    CoursewareBiographyAchievementsBlock,
+    CoursewareBiographyCareerBlock,
+    CoursewareBiographyGoalsBlock,
+    CoursewareBiographyPersonalInformationBlock,
+    CoursewareCanvasBlock,
+    CoursewareChartBlock,
+    CoursewareCodeBlock,
+    CoursewareConfirmBlock,
+    CoursewareDateBlock,
+    CoursewareDialogCardsBlock,
+    CoursewareDocumentBlock,
+    CoursewareDownloadBlock,
+    CoursewareEmbedBlock,
+    CoursewareErrorBlock,
+    CoursewareFolderBlock,
+    CoursewareGalleryBlock,
+    CoursewareHeadlineBlock,
+    CoursewareIframeBlock,
+    CoursewareImageMapBlock,
+    CoursewareKeyPointBlock,
+    CoursewareLinkBlock,
+    CoursewareLtiBlock,
+    CoursewareTableOfContentsBlock,
+    CoursewareTextBlock,
+    CoursewareTimelineBlock,
+    CoursewareTypewriterBlock,
+    CoursewareVideoBlock,
+    //layout
+    StudipIcon,
+    StudipProgressIndicator,
+};
+
+export default ContainerComponents;
diff --git a/resources/vue/components/courseware/content-icons.js b/resources/vue/components/courseware/content-icons.js
deleted file mode 100644
index ce5464c..0000000
--- a/resources/vue/components/courseware/content-icons.js
+++ /dev/null
@@ -1,114 +0,0 @@
-const contentIcons = [
-    'accept',
-    'add',
-    'add-circle',
-    'admin',
-    'aladdin',
-    'arr_1down',
-    'arr_1left',
-    'arr_1right',
-    'arr_1up',
-    'arr_2down',
-    'arr_2left',
-    'arr_2right',
-    'arr_2up',
-    'audio',
-    'audio3',
-    'billboard',
-    'block-canvas',
-    'block-comparison',
-    'block-eyecatcher',
-    'block-gallery',
-    'block-gallery2',
-    'block-imagemap',
-    'brainstorm',
-    'campusnavi',
-    'chat2',
-    'code',
-    'community2',
-    'computer',
-    'consultation',
-    'content',
-    'courseware',
-    'crown',
-    'date-single',
-    'decline',
-    'decline-circle',
-    'doctoral_cap',
-    'download',
-    'dropbox',
-    'edit',
-    'exclaim',
-    'exclaim-circle',
-    'export',
-    'favorite',
-    'filter',
-    'globe',
-    'graph',
-    'group2',
-    'group3',
-    'group4',
-    'home2',
-    'info',
-    'info-circle',
-    'install',
-    'institute',
-    'key',
-    'knife',
-    'learnmodule',
-    'lightbulb',
-    'lightbulb2',
-    'link2',
-    'link3',
-    'link-extern',
-    'link-intern',
-    'literature',
-    'lock-locked',
-    'lock-unlocked',
-    'mail2',
-    'medal',
-    'metro',
-    'microphone',
-    'module',
-    'network',
-    'notification',
-    'notification2',
-    'opencast',
-    'outer-space',
-    'permalink',
-    'person',
-    'phone',
-    'picture',
-    'place',
-    'plugin',
-    'question',
-    'question-circle',
-    'ranking',
-    'remove',
-    'remove-circle',
-    'resources',
-    'roles',
-    'schedule2',
-    'search',
-    'settings',
-    'span-empty',
-    'span-1quarter',
-    'span-2quarter',
-    'span-3quarter',
-    'span-full',
-    'spiral',
-    'sport',
-    'staple',
-    'star',
-    'star-empty',
-    'star-halffull',
-    'test',
-    'tools',
-    'topic',
-    'ufo',
-    'video2',
-    'visibility-visible',
-    'wizard'
-];
-
-export default contentIcons;
diff --git a/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue b/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
new file mode 100644
index 0000000..7415478
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
@@ -0,0 +1,57 @@
+<template>
+    <div class="cw-collapsible" :class="{ 'cw-collapsible-open': isOpen }">
+        <a href="#" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen">
+            <header :class="{ 'cw-collapsible-open': isOpen }" class="cw-collapsible-title">
+                <studip-icon v-if="icon" :shape="icon" /> {{ title }}
+            </header>
+        </a>
+        <div class="cw-collapsible-content" :class="{ 'cw-collapsible-content-open': isOpen }">
+            <slot></slot>
+        </div>
+    </div>
+</template>
+
+<script>
+import StudipIcon from '../../StudipIcon.vue';
+
+export default {
+    name: 'courseware-collapsible-box',
+    components: {
+        StudipIcon,
+    },
+    props: {
+        title: String,
+        icon: {
+            type: String,
+            default: '',
+        },
+        open: {
+            type: Boolean,
+            default: false,
+        },
+    },
+    data() {
+        return {
+            isOpen: this.open,
+        };
+    },
+    mounted(){
+        this.updateCollapsible();
+    },
+    updated() {
+        this.updateCollapsible();
+    },
+    methods: {
+        updateCollapsible() {
+            if (this.isOpen) {
+                STUDIP.eventBus.emit('courseware:update-collapsible', { 'uid': this._uid });
+            }
+        }
+    },
+    watch: {
+        open(state) {
+            this.isOpen = state;
+        }
+    }
+};
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue
new file mode 100644
index 0000000..f617e5a
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue
@@ -0,0 +1,24 @@
+<template>
+    <div class="cw-companion-box" :class="[mood]">
+        <div>
+            <p v-html="msgCompanion"></p>
+            <slot name="companionActions"></slot>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'courseware-companion-box',
+    props: {
+        msgCompanion: String,
+        mood: {
+            type: String,
+            default: 'default',
+            validator: value => {
+                return ['default','unsure', 'special', 'sad', 'pointing', 'curious'].includes(value);
+            }
+        }
+    },
+};
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
new file mode 100644
index 0000000..23fbf17
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
@@ -0,0 +1,49 @@
+<template>
+    <div
+        class="cw-companion-overlay"
+        :class="[showCompanion ? 'cw-companion-overlay-in' : '', showCompanion ? '' : 'cw-companion-overlay-out', styleCompanion]"
+    >
+        <div class="cw-companion-overlay-content" v-html="msgCompanion"></div>
+        <button class="cw-compantion-overlay-close" @click="hideCompanion"></button>
+    </div>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-companion-overlay',
+    computed: {
+        ...mapGetters({
+            showCompanion: 'showCompanionOverlay',
+            msgCompanion: 'msgCompanionOverlay',
+            styleCompanion: 'styleCompanionOverlay',
+            showToolbar: 'showToolbar',
+        }),
+    },
+    methods: {
+        ...mapActions({
+            coursewareShowCompanionOverlay: 'coursewareShowCompanionOverlay'
+        }),
+        hideCompanion() {
+            this.coursewareShowCompanionOverlay(false);
+        },
+    },
+    watch: {
+        showCompanion(newValue, oldValue) {
+            let view = this;
+            if (newValue === true && oldValue === false) {
+                setTimeout(() => {
+                    view.hideCompanion();
+                }, 4000);
+            }
+        },
+        showToolbar(newValue, oldValue) {
+            // hide companion when toolbar is closed 
+            if (oldValue === true && newValue === false) {
+                this.hideCompanion();
+            }
+        }
+    },
+};
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/layouts/CoursewareDateInput.vue b/resources/vue/components/courseware/layouts/CoursewareDateInput.vue
new file mode 100644
index 0000000..57a5619
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/CoursewareDateInput.vue
@@ -0,0 +1,34 @@
+<template>
+    <input :value="formattedDate" @input="onInput" type="date" />
+</template>
+
+<script>
+const fromISO8601 = (string) => new Date(string);
+const toISO8601 = (date) => date.toISOString();
+const pad = (what, length = 2) => `00000000${what}`.substr(-length);
+
+export default {
+    props: ['value'],
+    data: () => ({
+        date: new Date(),
+    }),
+    computed: {
+        formattedDate() {
+            return `${this.date.getFullYear()}-${pad(this.date.getMonth() + 1)}-${pad(this.date.getDate())}`;
+        },
+    },
+    methods: {
+        onInput({ target }) {
+            const newValue = toISO8601(target.valueAsDate);
+            if (newValue !== this.value) {
+                this.$emit('input', newValue);
+            }
+        },
+    },
+    beforeMount() {
+        if (this.value) {
+            this.date = fromISO8601(this.value);
+        }
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/layouts/CoursewareFileChooser.vue b/resources/vue/components/courseware/layouts/CoursewareFileChooser.vue
new file mode 100644
index 0000000..6571b34
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/CoursewareFileChooser.vue
@@ -0,0 +1,139 @@
+<template>
+    <div class="cw-file-chooser">
+        <span v-translate>Ordner-Filter</span>
+        <courseware-folder-chooser allowUserFolders unchoose v-model="selectedFolderId" />
+        <span v-translate>Datei</span>
+        <select v-model="currentValue" @change="selectFile">
+            <option v-show="canBeEmpty" value="">
+                <translate>Keine Auswahl</translate>
+            </option>
+            <option v-for="(file, index) in files" :key="index" :value="file.id">
+                {{ file.name }}
+            </option>
+            <option v-show="files.length === 0" disabled>
+                <translate>Keine Dateien vorhanden</translate>
+            </option>
+        </select>
+    </div>
+</template>
+
+<script>
+import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-file-chooser',
+    components: { CoursewareFolderChooser },
+    props: {
+        value: String,
+        mimeType: { type: String, default: '' },
+        isImage: { type: Boolean, default: false },
+        isVideo: { type: Boolean, default: false },
+        isAudio: { type: Boolean, default: false },
+        isDocument: { type: Boolean, default: false },
+        canBeEmpty: { type: Boolean, default: false },
+    },
+    data() {
+        return {
+            currentValue: '',
+            selectedFolderId: '',
+            files: [],
+        };
+    },
+    computed: {
+        ...mapGetters({
+            fileRefById: 'file-refs/byId',
+            relatedFileRefs: 'file-refs/related',
+            urlHelper: 'urlHelper',
+            userId: 'userId',
+            relatedTermOfUse: 'terms-of-use/related'
+        }),
+    },
+    methods: {
+        ...mapActions({
+            loadFileRef: 'file-refs/loadById',
+            loadRelatedFileRefs: 'file-refs/loadRelated',
+        }),
+        selectFile() {
+            this.$emit(
+                'selectFile',
+                this.files.find((file) => file.id === this.currentValue)
+            );
+        },
+        filterFiles(loadArray) {
+            const filterFile = (file) => {
+                let fileTermsOfUse = this.relatedTermOfUse({parent: file, relationship: 'terms-of-use'});
+
+                if (fileTermsOfUse !== null && fileTermsOfUse.attributes['download-condition'] !== 0) {
+                    return false;
+                }
+                if (this.mimeType !== '' && this.mimeType !== file.attributes['mime-type']) {
+                    return false;
+                }
+                if (this.isImage && !file.attributes['mime-type'].includes('image')) {
+                    return false;
+                }
+                const videoConditions = ['video/mp4', 'video/ogg', 'video/webm'];
+                if (this.isVideo && !videoConditions.some(condition => file.attributes['mime-type'].includes(condition))) {
+                    return false;
+                }
+                const audioConditions = ['audio/wav', 'audio/ogg', 'audio/webm','audio/flac', 'audio/mpeg', 'audio/x-m4a', 'audio/mp4'];
+                if (this.isAudio && !audioConditions.some(condition => file.attributes['mime-type'].includes(condition)) ) {
+                    return false;
+                }
+                const officeConditions = ['application/pdf']; //TODO enable more mime types
+                if (this.isDocument && !officeConditions.some(condition => file.attributes['mime-type'].includes(condition)) ) {
+                    return false;
+                }
+
+                return true;
+            };
+
+            return loadArray.filter(filterFile).map((file) => ({
+                id: file.id,
+                name: file.attributes.name,
+                mime_type: file.attributes['mime-type'],
+                download_url: this.urlHelper.getURL(
+                    'sendfile.php',
+                    { type: 0, file_id: file.id, file_name: file.attributes.name },
+                    true
+                ),
+            }));
+        },
+        async getFolderFiles() {
+            const parent = { type: 'folders', id: `${this.selectedFolderId}` };
+            const relationship = 'file-refs';
+            const options = { include: 'terms-of-use', 'page[limit]': 10000 };
+            await this.loadRelatedFileRefs({ parent, relationship, options });
+
+            const files = this.relatedFileRefs({ parent, relationship });
+            this.files = this.filterFiles(files);
+        },
+    },
+    async mounted() {
+        if (this.value != '') {
+            await this.loadFileRef({ id: this.value });
+            const fileRef = this.fileRefById({ id: this.value });
+
+            if (fileRef) {
+                this.selectedFolderId = fileRef.relationships.parent.data.id;
+                this.currentValue = this.value;
+            }
+        }
+    },
+    watch: {
+        selectedFolderId() {
+            if (this.selectedFolderId !== '') {
+                this.getFolderFiles();
+            }
+        },
+        value(newValue, oldValue) {
+            if (newValue === '') {
+                this.selectedFolderId = '';
+                this.currentValue = '';
+            }
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/layouts/CoursewareFolderChooser.vue b/resources/vue/components/courseware/layouts/CoursewareFolderChooser.vue
new file mode 100644
index 0000000..e071679
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/CoursewareFolderChooser.vue
@@ -0,0 +1,167 @@
+<template>
+    <select v-model="currentValue" @change="changeSelection">
+        <option v-if="unchoose" value=""><translate>kein Ordner ausgewählt</translate></option>
+        <optgroup v-if="this.context.type === 'courses'" :label="textOptGroupCourse">
+            <option v-for="folder in loadedCourseFolders" :key="folder.id" :value="folder.id">
+                {{ folder.attributes.name }}
+            </option>
+        </optgroup>
+        <optgroup v-if="allowUserFolders" :label="textOptGroupUser">
+            <option v-for="folder in loadedUserFolders" :key="folder.id" :value="folder.id">
+                {{ folder.attributes.name }}
+            </option>
+        </optgroup>
+    </select>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+function filterCourseFolders(folders, { allowHomeworkFolders }) {
+    const validatedParents = new Map();
+
+    return folders.filter((folder) => {
+        if (validateParentFolder(folder)) {
+            switch (folder.attributes['folder-type']) {
+                case 'HiddenFolder':
+                    if (folder.attributes['data-content']['download_allowed'] === 1) {
+                        return true;
+                    }
+                    break;
+                case 'HomeworkFolder':
+                    if (allowHomeworkFolders) {
+                        return true;
+                    }
+                    break;
+                default:
+                    return true;
+            }
+        }
+    });
+
+    function validateParentFolder(folder) {
+        let isValid = true;
+        if (folder?.relationships?.parent) {
+            let parentId = folder.relationships.parent.data.id;
+            if (validatedParents.has(parentId)) {
+                isValid = validatedParents.get(parentId);
+            } else {
+                let parent = folders.find((f) => f.id === parentId);
+                if (parent) {
+                    isValid = hiddenParentFolderValidation(parent);
+                    validatedParents.set(parentId, isValid);
+                }
+            }
+        }
+        return isValid;
+    }
+
+    function hiddenParentFolderValidation(parentFolder) {
+        if (parentFolder.attributes['folder-type'] === 'HiddenFolder') {
+            return false;
+        } else if (parentFolder?.relationships?.parent) {
+            // Recursively validating the parents.
+            return validateParentFolder(parentFolder);
+        } else {
+            return true;
+        }
+    }
+}
+export default {
+    name: 'courseware-folder-chooser',
+    props: {
+        value: String,
+        allowUserFolders: { type: Boolean, default: false },
+        allowHomeworkFolders: { type: Boolean, default: false },
+        unchoose: { type: Boolean, default: false },
+    },
+    data() {
+        return {
+            currentValue: Object,
+            textOptGroupCourse: this.$gettext('Dateibereich dieser Veranstaltung'),
+            textOptGroupUser: this.$gettext('Persönlicher Dateibereich'),
+        };
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            relatedFolders: 'folders/related',
+            userId: 'userId',
+        }),
+        courseObject() {
+            return { type: 'courses', id: `${this.context.id}` };
+        },
+        userObject() {
+            return { type: 'users', id: `${this.userId}` };
+        },
+        loadedCourseFolders() {
+            return filterCourseFolders(
+                this.relatedFolders({ parent: this.courseObject, relationship: 'folders' }) ?? [],
+                {
+                    allowHomeworkFolders: this.allowHomeworkFolders,
+                }
+            );
+        },
+        loadedUserFolders() {
+            let loadedUserFolders = [];
+            let UserFolders = this.relatedFolders({ parent: this.userObject, relationship: 'folders' }) ?? [];
+            UserFolders.forEach((folder) => {
+                if (folder.attributes['folder-type'] === 'PublicFolder') {
+                    loadedUserFolders.push(folder);
+                }
+            });
+
+            return loadedUserFolders;
+        },
+    },
+    methods: {
+        ...mapActions({
+            loadRelatedFolders: 'folders/loadRelated',
+        }),
+
+        changeSelection() {
+            this.$emit('input', this.currentValue);
+        },
+
+        getCourseFolders() {
+            const parent = this.courseObject;
+            const relationship = 'folders';
+            const options = { 'page[limit]': 10000 };
+
+            return this.loadRelatedFolders({ parent, relationship, options });
+        },
+
+        getUserFolders() {
+            const parent = this.userObject;
+            const relationship = 'folders';
+            const options = { 'page[limit]': 10000 };
+
+            return this.loadRelatedFolders({ parent, relationship, options });
+        },
+
+        confirmSelectedFolder() {
+            const folders = this.loadedUserFolders.concat(this.loadedCourseFolders);
+
+            let folder = folders.find( folder => folder.id === this.currentValue);
+
+            if (this.currentValue !== '' && folder === undefined) {
+                this.currentValue = '';
+                this.changeSelection();
+            }
+        }
+    },
+    async mounted() {
+        this.currentValue = this.value;
+        if (this.context.type !== 'users') {
+            await this.getCourseFolders();
+        }
+        await this.getUserFolders();
+        this.confirmSelectedFolder();
+    },
+    watch: {
+        value() {
+            this.currentValue = this.value;
+        }
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/layouts/CoursewareTab.vue b/resources/vue/components/courseware/layouts/CoursewareTab.vue
new file mode 100644
index 0000000..8d2e9bd
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/CoursewareTab.vue
@@ -0,0 +1,51 @@
+<template>
+    <div
+        role="tabpanel"
+        class="cw-tab"
+        :id="id"
+        :class="{ 'cw-tab-active': isActive }"
+        :aria-labelledby="selectorId"
+    >
+        <slot></slot>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'courseware-tab',
+    props: {
+        name: {type: String, required: true },
+        alias: {type: String, default: ''},
+        selected: { type: Boolean, default: false },
+        index: {type: Number, required: true },
+        icon: {type: String, default: ''},
+    },
+    data() {
+        return {
+            isActive: false,
+        };
+    },
+    computed: {
+        selectorId() {
+            return '#' + this._uid + '-' + this.name.toLowerCase().replace(/ /g, '-');
+        },
+        id() {
+            return this._uid + '-' + this.name.toLowerCase().replace(/ /g, '-') + '-tabpanel';
+        }
+    },
+    mounted() {
+        this.isActive = this.selected;
+    },
+    updated () {
+        if (this.isActive) {
+            STUDIP.eventBus.emit('courseware:update-tab',{ 'uid': this._uid });
+        }
+
+    },
+    watch: {
+        selected(newValue) {
+            this.isActive = newValue;
+        }
+    }
+};
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/layouts/CoursewareTabs.vue b/resources/vue/components/courseware/layouts/CoursewareTabs.vue
new file mode 100644
index 0000000..5bdcc47
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/CoursewareTabs.vue
@@ -0,0 +1,133 @@
+<template>
+    <div class="cw-tabs">
+        <div role="tablist" class="cw-tabs-nav">
+            <button
+                v-for="(tab, index) in tabs"
+                :key="index"
+                :class="[
+                    tab.isActive ? 'is-active' : '',
+                    tab.icon !== '' && tab.name !== '' ? 'cw-tabs-nav-icon-text-' + tab.icon : '',
+                    tab.icon !== '' && tab.name === '' ? 'cw-tabs-nav-icon-solo-' + tab.icon : '',
+                ]"
+                :aria-selected="tab.isActive"
+                :aria-controls="tab.id"
+                :id="tab.selectorId"
+                :tabindex="tab.isActive ? 0 : -1"
+                @click="selectTab(tab.selectorId)"
+                @keydown="handleKeyEvent($event)"
+                :ref="tab.selectorId"
+            >
+                {{ tab.name }}
+            </button>
+        </div>
+        <div class="cw-tabs-content">
+            <slot></slot>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'courseware-tabs',
+    props: {
+        setSelected: { type: Number, required: false, default: 0 },
+    },
+    data() {
+        return {
+            tabs: [],
+        };
+    },
+    created() {
+        this.tabs = this.$children.sort((a, b) => {
+            return a.index - b.index;
+        });
+    },
+    methods: {
+        selectTab(selectorId) {
+            let view = this;
+            this.tabs.forEach((tab) => {
+                tab.isActive = false;
+                if (tab.selectorId === selectorId) {
+                    tab.isActive = true;
+                    view.$refs[selectorId][0].focus();
+                    view.$emit('selectTab', tab);
+                }
+            });
+        },
+        selectTabByIndex(selectedIndex) {
+            let view = this;
+            this.tabs.forEach((tab) => {
+                tab.isActive = false;
+                if (tab.index === selectedIndex) {
+                    tab.isActive = true;
+                    view.$refs[tab.selectorId][0].focus();
+                    view.$emit('selectTab', tab);
+                }
+            });
+        },
+        handleKeyEvent(e) {
+            let tablist = e.target.parentElement;
+            switch (e.keyCode) {
+                case 37: // left
+                case 38: // up
+                    if (tablist.firstChild.id !== e.target.id) {
+                        this.selectTab(e.target.previousElementSibling.id);
+                    } else {
+                        this.selectTab(tablist.lastChild.id);
+                    }
+                    break;
+                case 39: // right
+                case 40: // down
+                    if (tablist.lastChild.id !== e.target.id) {
+                        this.selectTab(e.target.nextElementSibling.id);
+                    } else {
+                        this.selectTab(tablist.firstChild.id);
+                    }
+                    break;
+                case 36: //pos1
+                    this.selectTab(tablist.firstChild.id);
+                    break;
+                case 35: //end
+                    this.selectTab(tablist.lastChild.id);
+                    break;
+            }
+        },
+        getButtonById(id) {
+            return this.$refs[id][0];
+        },
+        getFirstTabButton() {
+            let selectorId = this.tabs[0].selectorId;
+            return this.$refs[selectorId][0];
+        },
+        getTabButtonByName(name) {
+            let selectorId = null;
+            this.tabs.forEach((tab) => {
+                if (tab.name === name) {
+                    selectorId = tab.selectorId;
+                }
+            });
+            if (selectorId) {
+                return this.$refs[selectorId][0];
+            }
+            return null;
+        },
+        getTabButtonByAlias(alias) {
+            let selectorId = null;
+            this.tabs.forEach((tab) => {
+                if (tab.alias === alias) {
+                    selectorId = tab.selectorId;
+                }
+            });
+            if (selectorId) {
+                return this.$refs[selectorId][0];
+            }
+            return null;
+        },
+    },
+    watch: {
+        setSelected(tab) {
+            this.selectTabByIndex(tab);
+        },
+    },
+};
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue b/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue
new file mode 100644
index 0000000..cd76aaa
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue
@@ -0,0 +1,26 @@
+<template>
+    <div :class="{ 'cw-talk-bubble-own-post': payload.own }" class="cw-talk-bubble">
+        <div class="cw-talk-bubble-user" v-if="!payload.own">
+            <div class="cw-talk-bubble-avatar">
+                <img :src="payload.user_avatar" />
+            </div>
+            <span>{{ payload.user_name }}</span>
+        </div>
+        <div class="cw-talk-bubble-talktext">
+            <p>{{ payload.content }}</p>
+            <p class="cw-talk-bubble-talktext-time"><iso-date :date="payload.chdate" /></p>
+        </div>
+    </div>
+</template>
+
+<script>
+import IsoDate from './IsoDate.vue';
+
+export default {
+    name: 'courseware-talk-bubble',
+    components: { IsoDate },
+    props: {
+        payload: Object,
+    },
+};
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/layouts/CoursewareTile.vue b/resources/vue/components/courseware/layouts/CoursewareTile.vue
new file mode 100644
index 0000000..f396769
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/CoursewareTile.vue
@@ -0,0 +1,145 @@
+<template>
+    <component :is="tag" class="cw-tile" :class="[color]">
+        <div
+            class="preview-image"
+            :class="[hasImage ? '' : 'default-image']"
+            :style="previewImageStyle"
+        >
+            <div class="overlay-text" v-if="hasImageOverlay">
+                <slot name="image-overlay"></slot>
+            </div>
+            <div class="overlay-action-menu" v-if="hasImageOverlayWithActionMenu">
+                <slot name="image-overlay-with-action-menu"></slot>
+            </div>
+        </div>
+        <component
+            :is="hasDescriptionLink ? 'a' : 'div'"
+            :href="hasDescriptionLink ? descriptionLink : ''"
+            :title="descriptionTitle"
+            class="description"
+        >
+            <header
+                :class="[icon ? 'description-icon-' + icon : '']"
+            >
+                {{ title }}
+            </header>
+            <div
+                v-if="displayProgress"
+                :title="progressTitle"
+                class="progress-wrapper" >
+                <progress :value="progress" max="100">{{ progress }}</progress>
+            </div>
+            <div class="description-text-wrapper">
+                <p><slot name="description"></slot></p>
+            </div>
+            <footer>
+                <slot name="footer"></slot>
+            </footer>
+        </component>
+    </component>
+</template>
+
+<script>
+import { mapGetters } from 'vuex';
+
+export default {
+    name: "courseware-tile",
+    props: {
+        tag: {
+            type: String,
+            default: "div",
+            validator: tag => {
+                return ["div", "li"].includes(tag);
+            }
+        },
+        color: {
+            type: String,
+            default: "studip-blue",
+            validator: value => {
+                return [
+                    "black",
+                    "charcoal",
+                    "royal-purple",
+                    "iguana-green",
+                    "queen-blue",
+                    "verdigris",
+                    "mulberry",
+                    "pumpkin",
+                    "sunglow",
+                    "apple-green",
+                    "studip-blue",
+                    "studip-lightblue",
+                    "studip-green",
+                    "studip-yellow",
+                    "studip-gray",
+                ].includes(value);
+            }
+        },
+        title: {
+            type: String,
+            default: "–"
+        },
+        icon: {
+            type: String
+        },
+        imageUrl: {
+            type: String
+        },
+        displayProgress: {
+            type: Boolean,
+            default: false
+        },
+        progress: {
+            type: Number,
+            validator: value => {
+                return value >= 0 && value <= 100;
+            }
+        },
+        descriptionLink: {
+            type: String,
+            default: ""
+        },
+        descriptionTitle: {
+            type: String,
+            default: ''
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userIsTeacher: 'userIsTeacher'
+        }),
+        hasImage() {
+            return this.imageUrl !== "" && this.imageUrl !== undefined;
+        },
+        hasImageOverlay() {
+            return this.$slots["image-overlay"] !== undefined;
+        },
+        hasImageOverlayWithActionMenu() {
+            return this.$slots["image-overlay-with-action-menu"] !== undefined;
+        },
+        previewImageStyle() {
+            if (this.hasImage) {
+                return { "background-image": "url(" + this.imageUrl + ")" };
+            }
+            else {
+                return {};
+            }
+        },
+        progressTitle() {
+            if (this.userIsTeacher) {
+                return this.$gettextInterpolate(this.$gettext("Fortschritt aller Teilnehmenden: %{progress}%"), { progress: this.progress });    
+            }
+            return this.$gettextInterpolate(this.$gettext("Mein Fortschritt: %{progress}%"), { progress: this.progress });
+        },
+        hasDescriptionLink() {
+            return this.descriptionLink !== '';
+        }
+    },
+    methods: {
+        showProgress(e) {
+            e.preventDefault();
+            this.$emit("showProgress");
+        }
+    },
+}
+</script>
diff --git a/resources/vue/components/courseware/layouts/IsoDate.vue b/resources/vue/components/courseware/layouts/IsoDate.vue
new file mode 100644
index 0000000..4ab6840
--- /dev/null
+++ b/resources/vue/components/courseware/layouts/IsoDate.vue
@@ -0,0 +1,17 @@
+<template>
+    <studip-date-time :timestamp="unixTimestamp" />
+</template>
+
+<script>
+export default {
+    name: 'courseware-iso-date',
+    props: {
+        date: String,
+    },
+    computed: {
+        unixTimestamp() {
+            return +new Date(this.date) / 1000;
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/plugin-components.js b/resources/vue/components/courseware/plugin-components.js
index e3f236e..b0e371b 100644
--- a/resources/vue/components/courseware/plugin-components.js
+++ b/resources/vue/components/courseware/plugin-components.js
@@ -1,11 +1,11 @@
-import CoursewareBlockAdderArea from './CoursewareBlockAdderArea.vue';
-import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
-import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
-import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-import CoursewareDefaultContainer from './CoursewareDefaultContainer.vue';
-import CoursewareFileChooser from './CoursewareFileChooser.vue';
-import CoursewareTabs from './CoursewareTabs.vue';
-import CoursewareTab from './CoursewareTab.vue';
+import CoursewareBlockAdderArea from './containers/CoursewareBlockAdderArea.vue';
+import CoursewareCollapsibleBox from './layouts/CoursewareCollapsibleBox.vue';
+import CoursewareCompanionBox from './layouts/CoursewareCompanionBox.vue';
+import CoursewareDefaultBlock from './blocks/CoursewareDefaultBlock.vue';
+import CoursewareDefaultContainer from './containers/CoursewareDefaultContainer.vue';
+import CoursewareFileChooser from './layouts/CoursewareFileChooser.vue';
+import CoursewareTabs from './layouts/CoursewareTabs.vue';
+import CoursewareTab from './layouts/CoursewareTab.vue';
 
 const CoursewarePluginComponents = {
     CoursewareBlockAdderArea,
diff --git a/resources/vue/components/courseware/structural-element/CoursewareBlockadderItem.vue b/resources/vue/components/courseware/structural-element/CoursewareBlockadderItem.vue
new file mode 100644
index 0000000..921014a
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareBlockadderItem.vue
@@ -0,0 +1,115 @@
+<template>
+    <div class="cw-blockadder-item-wrapper">
+        <a href="#" @click.prevent="addBlock" class="cw-blockadder-item" :class="['cw-blockadder-item-' + type]">
+            <header class="cw-blockadder-item-title">
+                {{ title }}
+            </header>
+            <p class="cw-blockadder-item-description">
+                {{ description }}
+            </p>
+        </a>
+        <button
+            class="cw-blockadder-item-fav"
+            :title="favButtonTitle"
+            @click="toggleFavItem()"
+        >
+            <studip-icon :shape="blockTypeIsFav ? 'star' : 'star-empty'" :size="20" />
+        </button>
+    </div>
+    
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-blockadder-item',
+    components: {},
+    props: {
+        title: String,
+        description: String,
+        type: String,
+    },
+    data() {
+        return {
+            showInfo: false,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            blockAdder: 'blockAdder',
+            blockById: 'courseware-blocks/byId',
+            lastCreatedBlock: 'courseware-blocks/lastCreated',
+            favoriteBlockTypes: 'favoriteBlockTypes',
+        }),
+        blockTypeIsFav() {
+            return this.favoriteBlockTypes.some((type) => type.type === this.type);
+        },
+        favButtonTitle() {
+            if (this.blockTypeIsFav) {
+                return this.$gettextInterpolate(
+                    this.$gettext('%{ blockName } Block aus den Favoriten entfernen'),
+                    { blockName: this.title }
+                );
+            }
+
+            return this.$gettextInterpolate(
+                    this.$gettext('%{ blockName } Block zu Favoriten hinzufügen'),
+                    { blockName: this.title }
+                );   
+        }
+    },
+    methods: {
+        ...mapActions({
+            companionInfo: 'companionInfo',
+            companionSuccess: 'companionSuccess',
+            companionWarning: 'companionWarning',
+            createBlock: 'createBlockInContainer',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+            loadBlock: 'courseware-blocks/loadById',
+            updateContainer: 'updateContainer',
+            removeFavoriteBlockType: 'removeFavoriteBlockType',
+            addFavoriteBlockType: 'addFavoriteBlockType',
+        }),
+        async addBlock() {
+            if (Object.keys(this.blockAdder).length !== 0) {
+                // lock parent container
+                await this.lockObject({ id: this.blockAdder.container.id, type: 'courseware-containers' });
+                // create new block
+                await this.createBlock({
+                    container: this.blockAdder.container,
+                    section: this.blockAdder.section,
+                    blockType: this.type,
+                });
+                //get new Block
+                const newBlock = this.lastCreatedBlock;
+                // update container information -> new block id in sections
+                let container = this.blockAdder.container;
+                container.attributes.payload.sections[this.blockAdder.section].blocks.push(newBlock.id);
+                const structuralElementId = container.relationships['structural-element'].data.id;
+                // update container
+                await this.updateContainer({ container, structuralElementId });
+                // unlock container
+                await this.unlockObject({ id: this.blockAdder.container.id, type: 'courseware-containers' });
+                this.companionSuccess({
+                    info: this.$gettext('Der Block wurde erfolgreich eingefügt.'),
+                });
+                this.$emit('blockAdded');
+            } else {
+                // companion action
+                this.companionWarning({
+                    info: this.$gettext('Bitte wählen Sie einen Ort aus, an dem der Block eingefügt werden soll.'),
+                });
+            }
+        },
+        toggleFavItem() {
+            if (this.blockTypeIsFav) {
+                this.removeFavoriteBlockType(this.type);
+            } else {
+                this.addFavoriteBlockType(this.type);
+            }
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareClipboardItem.vue b/resources/vue/components/courseware/structural-element/CoursewareClipboardItem.vue
new file mode 100644
index 0000000..aa975c8
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareClipboardItem.vue
@@ -0,0 +1,246 @@
+<template>
+    <div class="cw-clipboard-item-wrapper">
+        <button class="cw-clipboard-item" :class="['cw-clipboard-item-' + kind]" @click.prevent="insertItem">
+            <header class="sr-only">
+                {{ srTitle }}
+            </header>
+            <header class="cw-clipboard-item-title" aria-hidden="true">
+                {{ name }}
+            </header>
+            <p class="cw-clipboard-item-description">
+                {{ description }}
+            </p>
+        </button>
+        <div class="cw-clipboard-item-action-menu-wrapper">
+            <studip-action-menu
+                class="cw-clipboard-item-action-menu"
+                :items="menuItems"
+                :context="name"
+                @insertItemCopy="insertItemCopy"
+                @editItem="showEditItem"
+                @deleteItem="deleteItem"
+            />
+        </div>
+        <studip-dialog
+            v-if="showEditDialog"
+            :title="$gettext('Umbenennen')"
+            :confirmText="$gettext('Speichern')"
+            confirmClass="accept"
+            :closeText="$gettext('Abbrechen')"
+            closeClass="cancel"
+            height="360"
+            width="500"
+            @close="closeEditItem"
+            @confirm="storeItem"
+        >
+            <template v-slot:dialogContent>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Titel') }}
+                        <input type="text" v-model="currentClipboard.attributes.name" />
+                    </label>
+                    <label>
+                        {{ $gettext('Beschreibung') }}
+                        <textarea v-model="currentClipboard.attributes.description"></textarea>
+                    </label>
+                </form>
+            </template>
+        </studip-dialog>
+    </div>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-clipboard-item',
+    components: {},
+    props: {
+        clipboard: Object,
+    },
+    data() {
+        return {
+            showEditDialog: false,
+            currentClipboard: null,
+
+            text: {
+                errorMessage: this.$gettext('Es ist ein Fehler aufgetreten.'),
+                positionWarning: this.$gettext(
+                    'Bitte wählen Sie einen Ort aus, an dem der Block eingefügt werden soll.'
+                ),
+                blockSuccess: this.$gettext('Der Block wurde erfolgreich eingefügt.'),
+                containerSuccess: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'),
+            },
+        };
+    },
+    computed: {
+        ...mapGetters({
+            blockAdder: 'blockAdder',
+            currentElement: 'currentElement',
+        }),
+        name() {
+            return this.clipboard.attributes.name;
+        },
+        description() {
+            return this.clipboard.attributes.description;
+        },
+        isBlock() {
+            return this.clipboard.attributes['object-type'] === 'courseware-blocks';
+        },
+        kind() {
+            return this.clipboard.attributes['object-kind'];
+        },
+        blockId() {
+            return this.clipboard.attributes['block-id'];
+        },
+        blockNotFound() {
+            return this.clipboard.relationships.block.data === null;
+        },
+        containerId() {
+            return this.clipboard.attributes['container-id'];
+        },
+        containerNotFound() {
+            return this.clipboard.relationships.container.data === null;
+        },
+        itemNotFound() {
+            if (this.isBlock) {
+                return this.blockNotFound;
+            }
+
+            return this.containerNotFound;
+        },
+        menuItems() {
+            let menuItems = [];
+            if (!this.itemNotFound) {
+                menuItems.push({
+                    id: 1,
+                    label: this.$gettext('Kopie des aktuellen Stands einfügen'),
+                    icon: 'copy',
+                    emit: 'insertItemCopy',
+                });
+            }
+            menuItems.push({ id: 2, label: this.$gettext('Umbenennen'), icon: 'edit', emit: 'editItem' });
+            menuItems.push({ id: 3, label: this.$gettext('Löschen'), icon: 'trash', emit: 'deleteItem' });
+
+            menuItems.sort((a, b) => a.id - b.id);
+            return menuItems;
+        },
+        blockAdderActive() {
+            return Object.keys(this.blockAdder).length !== 0;
+        },
+        srTitle() {
+            return this.isBlock ? 
+                this.$gettextInterpolate(this.$gettext(`Block %{name} einfügen`), { name: this.name }) :
+                this.$gettextInterpolate(this.$gettext(`Abschnitt %{name} einfügen`), { name: this.name });
+        }
+    },
+    methods: {
+        ...mapActions({
+            companionInfo: 'companionInfo',
+            companionSuccess: 'companionSuccess',
+            companionWarning: 'companionWarning',
+            copyContainer: 'copyContainer',
+            copyBlock: 'copyBlock',
+            clipboardInsertBlock: 'clipboardInsertBlock',
+            clipboardInsertContainer: 'clipboardInsertContainer',
+            loadStructuralElement: 'loadStructuralElement',
+            loadContainer: 'loadContainer',
+            deleteClipboard: 'courseware-clipboards/delete',
+            updateClipboard: 'courseware-clipboards/update',
+            loadClipboard: 'courseware-clipboards/loadById',
+        }),
+
+        async insertItem() {
+            let insertError = false;
+
+            if (this.isBlock) {
+                if (!this.blockAdderActive) {
+                    this.companionWarning({ info: this.text.positionWarning });
+                    return;
+                }
+                try {
+                    await this.clipboardInsertBlock({
+                        parentId: this.blockAdder.container.id,
+                        section: this.blockAdder.section,
+                        clipboard: this.clipboard,
+                    });
+                } catch (error) {
+                    insertError = true;
+                    this.companionWarning({ info: this.text.errorMessage });
+                }
+                if (!insertError) {
+                    await this.loadContainer(this.blockAdder.container.id);
+                    this.companionSuccess({ info: this.text.blockSuccess });
+                }
+            } else {
+                try {
+                    await this.clipboardInsertContainer({
+                        parentId: this.currentElement,
+                        clipboard: this.clipboard,
+                    });
+                } catch (error) {
+                    insertError = true;
+                    this.companionWarning({ info: this.text.errorMessage });
+                }
+                if (!insertError) {
+                    this.loadStructuralElement(this.currentElement);
+                    this.companionSuccess({ info: this.text.containerSuccess });
+                }
+            }
+        },
+
+        async insertItemCopy() {
+            let insertError = false;
+
+            if (this.isBlock) {
+                if (!this.blockAdderActive) {
+                    this.companionWarning({ info: this.text.positionWarning });
+                    return;
+                }
+                try {
+                    await this.copyBlock({
+                        parentId: this.blockAdder.container.id,
+                        section: this.blockAdder.section,
+                        block: { id: this.blockId },
+                    });
+                } catch (error) {
+                    insertError = true;
+                    this.companionWarning({ info: this.text.errorMessage });
+                }
+                if (!insertError) {
+                    await this.loadContainer(this.blockAdder.container.id);
+                    this.companionSuccess({ info: this.text.blockSuccess });
+                }
+            } else {
+                try {
+                    await this.copyContainer({ parentId: this.currentElement, container: { id: this.containerId } });
+                } catch (error) {
+                    insertError = true;
+                    this.companionWarning({ info: this.text.errorMessage });
+                }
+                if (!insertError) {
+                    this.loadStructuralElement(this.currentElement);
+                    this.companionSuccess({ info: this.text.containerSuccess });
+                }
+            }
+        },
+        deleteItem() {
+            this.deleteClipboard({ id: this.clipboard.id });
+        },
+        showEditItem() {
+            this.showEditDialog = true;
+        },
+        closeEditItem() {
+            this.showEditDialog = false;
+        },
+        async storeItem() {
+            this.closeEditItem();
+            await this.updateClipboard(this.currentClipboard);
+            this.loadClipboard({ id: this.currentClipboard.id });
+        },
+    },
+    mounted() {
+        this.currentClipboard = _.cloneDeep(this.clipboard);
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareEmptyElementBox.vue b/resources/vue/components/courseware/structural-element/CoursewareEmptyElementBox.vue
new file mode 100644
index 0000000..062fa57
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareEmptyElementBox.vue
@@ -0,0 +1,58 @@
+<template>
+    <div class="cw-welcome-screen">
+        <courseware-companion-box :msgCompanion="this.$gettext('Es wurden bisher noch keine Inhalte eingepflegt.')">
+            <template v-slot:companionActions>
+                <button v-if="canEdit && noContainers" class="button" @click="addContainer"><translate>Einen Abschnitt hinzufügen</translate></button>
+                <button v-if="canEdit && !noContainers && !editMode" class="button" @click="switchToEditView"><translate>Seite bearbeiten</translate></button>
+            </template>
+        </courseware-companion-box>
+    </div>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-empty-element-box',
+    components: {
+        CoursewareCompanionBox,
+    },
+    props: {
+        canEdit: Boolean,
+        noContainers: Boolean
+    },
+    data() {
+        return{}
+    },
+    computed: {
+        ...mapGetters({
+            viewMode: 'viewMode'
+        }),
+        editMode() {
+            return this.viewMode === 'edit';
+        }
+    },
+    methods: {
+        ...mapActions({
+            coursewareViewMode: 'coursewareViewMode',
+            coursewareConsumeMode: 'coursewareConsumeMode',
+            coursewareContainerAdder: 'coursewareContainerAdder',
+            coursewareSelectedToolbarItem: 'coursewareSelectedToolbarItem',
+            coursewareShowToolbar: 'coursewareShowToolbar'
+        }),
+        addContainer() {
+            this.coursewareViewMode('edit');
+            this.coursewareConsumeMode(false);
+            this.coursewareContainerAdder(true);
+            this.coursewareSelectedToolbarItem('blockadder');
+            this.coursewareShowToolbar(true);
+        },
+        switchToEditView() {
+            this.coursewareViewMode('edit');
+            this.coursewareConsumeMode(false);
+        }
+    }
+
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
new file mode 100644
index 0000000..ddf5c9b
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
@@ -0,0 +1,170 @@
+<template>
+    <div :class="{ 'cw-ribbon-wrapper-consume': consumeMode }" :id="isContentBar ? 'contentbar' : null" >
+        <div v-show="stickyRibbon" class="cw-ribbon-sticky-top"></div>
+        <header class="cw-ribbon" :class="{ 'cw-ribbon-sticky': stickyRibbon, 'cw-ribbon-consume': consumeMode }">
+            <div class="cw-ribbon-wrapper-left">
+                <nav class="cw-ribbon-nav" :class="buttonsClass">
+                    <slot name="buttons" />
+                </nav>
+                <nav class="cw-ribbon-breadcrumb">
+                    <ul>
+                        <slot v-if="breadcrumbFallback" name="breadcrumbFallback" />
+                        <slot v-else name="breadcrumbList" />
+                    </ul>
+                </nav>
+            </div>
+            <div class="cw-ribbon-wrapper-right">
+                <button
+                    v-if="showToolbarButton"
+                    class="cw-ribbon-button cw-ribbon-button-menu"
+                    :title="textRibbon.toolbar"
+                    @click.prevent="activeToolbar"
+                >
+                </button>
+                <slot name="menu" />
+            </div>
+            <div v-if="consumeMode" class="cw-ribbon-consume-bottom"></div>
+            <courseware-ribbon-toolbar
+                v-if="showTools"
+                :toolsActive="unfold"
+                :stickyRibbon="stickyRibbon"
+                :class="{ 'cw-ribbon-tools-sticky': stickyRibbon }"
+                :style="{ height: toolbarHeight + 'px' }"
+                :canEdit="canEdit"
+                @deactivate="deactivateToolbar"
+                @blockAdded="$emit('blockAdded')"
+            />
+        </header>
+        <div v-if="stickyRibbon" class="cw-ribbon-sticky-bottom"></div>
+        <div v-if="stickyRibbon" class="cw-ribbon-sticky-spacer"></div>
+    </div>
+</template>
+
+<script>
+import CoursewareRibbonToolbar from './CoursewareRibbonToolbar.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-ribbon',
+    components: {
+        CoursewareRibbonToolbar,
+    },
+    props: {
+        canEdit: Boolean,
+        showToolbarButton: {
+            default: true,
+            type: Boolean
+        },
+        showModeSwitchButton: {
+            default: true,
+            type: Boolean
+        },
+        buttonsClass: String,
+        isContentBar: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data() {
+        return {
+            readModeActive: false,
+            stickyRibbon: false,
+            textRibbon: {
+                toolbar: this.$gettext('Inhaltsverzeichnis'),
+                fullscreen_on: this.$gettext('Fokusmodus einschalten'),
+                fullscreen_off: this.$gettext('Fokusmodus ausschalten'),
+            },
+            unfold: false,
+            showTools: false,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            consumeMode: 'consumeMode',
+            toolsActive: 'showToolbar'
+        }),
+        breadcrumbFallback() {
+            return window.outerWidth < 1200;
+        },
+        toolbarHeight() {
+            if (this.stickyRibbon) {
+                return parseInt(window.innerHeight * 0.75);
+            } else {
+                return parseInt(Math.min(window.innerHeight * 0.75, window.innerHeight - 197));
+            }
+        }
+    },
+    methods: {
+        ...mapActions({
+            coursewareConsumeMode: 'coursewareConsumeMode',
+            coursewareSelectedToolbarItem: 'coursewareSelectedToolbarItem',
+            coursewareViewMode: 'coursewareViewMode',
+            coursewareShowToolbar: 'coursewareShowToolbar'
+
+        }),
+        toggleConsumeMode() {
+            STUDIP.eventBus.emit('toggle-focus-mode', !this.consumeMode);
+            if (!this.consumeMode) {
+                document.body.classList.add('consuming_mode');
+                this.coursewareConsumeMode(true);
+                this.coursewareSelectedToolbarItem('contents');
+                this.coursewareViewMode('read');
+            } else {
+                this.coursewareConsumeMode(false);
+                document.body.classList.remove('consuming_mode');
+            }
+        },
+        activeToolbar() {
+            this.coursewareShowToolbar(true);
+        },
+        deactivateToolbar() {
+            this.coursewareShowToolbar(false);
+        },
+        handleScroll() {
+            if (window.outerWidth > 767) {
+                this.stickyRibbon = window.scrollY > 128 && !this.consumeMode;
+            } else {
+                this.stickyRibbon = window.scrollY > 75 && !this.consumeMode;
+            }
+        },
+    },
+    mounted() {
+        window.addEventListener('scroll', this.handleScroll);
+        if (this.isContentBar) {
+            STUDIP.eventBus.emit('courseware-contentbar-mounted', this);
+        }
+
+        this.globalOn('switch-focus-mode', (state) => {
+            if (state !== this.consumeMode) {
+                this.toggleConsumeMode();
+            }
+        });
+    },
+    beforeDestroy() {
+        STUDIP.eventBus.off('switch-focus-mode');
+    },
+    watch: {
+        toolsActive(newState, oldState) {
+            let view = this;
+            if(newState) {
+                this.showTools = true;
+                setTimeout(() => {view.unfold = true}, 10);
+            } else {
+                this.unfold = false;
+                setTimeout(() => {
+                    if(!view.toolsActive) {
+                        view.showTools = false;
+                    }
+                }, 800);
+            }
+        },
+        consumeMode(newState) {
+            if (newState) {
+                document.body.classList.add('consuming_mode');
+            } else {
+                document.body.classList.remove('consuming_mode');
+            }
+        }
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
new file mode 100644
index 0000000..164dcae
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
@@ -0,0 +1,206 @@
+<template>
+    <focus-trap v-model="trap" :initial-focus="() => initialFocusElement" :clickOutsideDeactivates="true" :fallbackFocus ="() => fallbackFocusElement">
+        <div
+            class="cw-ribbon-tools"
+            :class="{ unfold: toolsActive, 'cw-ribbon-tools-consume': consumeMode }"
+        >
+            <div class="cw-ribbon-tool-content">
+                <div class="cw-ribbon-tool-content-nav">
+                    <courseware-tabs
+                        class="cw-ribbon-tool-content-tablist"
+                        ref="tabs"
+                        @selectTab="selectTool($event.alias)"
+                    >
+                        <courseware-tab
+                            :name="$gettext('Inhaltsverzeichnis')"
+                            :selected="showContents"
+                            alias="contents"
+                            ref="contents"
+                            :index="0"
+                        >
+                            <courseware-tools-contents
+                                id="cw-ribbon-tool-contents"
+                            />
+                        </courseware-tab>
+                        <courseware-tab
+                            v-if="displayAdder"
+                            :name="$gettext('Elemente hinzufügen')"
+                            :selected="showBlockAdder"
+                            alias="blockadder"
+                            class="cw-ribbon-tool-blockadder-tab"
+                            :index="1"
+                        >
+                            <courseware-tools-blockadder
+                                v-if="showBlockAdder"
+                                id="cw-ribbon-tool-blockadder"
+                                :stickyRibbon="stickyRibbon"
+                                @blockAdded="$emit('blockAdded')"
+                            />
+                        </courseware-tab>
+                    </courseware-tabs>
+                    <button
+                        :title="$gettext('schließen')"
+                        class="cw-tools-hide-button"
+                        ref="closeTools"
+                        @click="$emit('deactivate')">
+                    </button>
+                </div>
+            </div>
+        </div>
+    </focus-trap>
+</template>
+<script>
+import CoursewareTabs from '../layouts/CoursewareTabs.vue';
+import CoursewareTab from '../layouts/CoursewareTab.vue';
+import CoursewareToolsBlockadder from './CoursewareToolsBlockadder.vue';
+import CoursewareToolsContents from './CoursewareToolsContents.vue';
+import { FocusTrap } from 'focus-trap-vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-ribbon-toolbar',
+    components: {
+        CoursewareTabs,
+        CoursewareTab,
+        CoursewareToolsBlockadder,
+        CoursewareToolsContents,
+        FocusTrap,
+    },
+    props: {
+        toolsActive: Boolean,
+        canEdit: Boolean,
+        disableSettings: {
+            type: Boolean,
+            default: false,
+        },
+        disableAdder: {
+            type: Boolean,
+            default: false,
+        },
+        stickyRibbon: {
+            type: Boolean,
+            default: false,
+        },
+    },
+    data() {
+        return {
+            showContents: true,
+            showBlockAdder: false,
+            trap: false,
+            initialFocusElement: null
+        };
+    },
+    computed: {
+        ...mapGetters({
+            userIsTeacher: 'userIsTeacher',
+            consumeMode: 'consumeMode',
+            containerAdder: 'containerAdder',
+            adderStorage: 'blockAdder',
+            viewMode: 'viewMode',
+            context: 'context',
+            userById: 'users/byId',
+            userId: 'userId',
+            selectedToolbarItem: 'selectedToolbarItem',
+            currentElementisLink: 'currentElementisLink',
+        }),
+        showEditMode() {
+            return this.viewMode === 'edit';
+        },
+        displayAdder() {
+            if (this.disableAdder) {
+                return false;
+            } else {
+                return !this.consumeMode && this.showEditMode && this.canEdit && !this.currentElementisLink;
+            }
+        },
+        displaySettings() {
+            if (this.disableSettings) {
+                return false;
+            } else {
+                let user = this.userById({ id: this.userId });
+                return !this.consumeMode && this.context.type === 'courses' && (this.isTeacher || ['root', 'admin'].includes(user.attributes.permission));
+            }
+            
+        },
+        isTeacher() {
+            return this.userIsTeacher;
+        },
+        fallbackFocusElement(){
+            return this.$refs.contents;
+        }
+    },
+    methods: {
+        ...mapActions({
+            setToolbarItem: 'coursewareSelectedToolbarItem',
+            coursewareContainerAdder: 'coursewareContainerAdder'
+        }),
+        selectTool(alias) {
+            this.showContents = false;
+            this.showBlockAdder = false;
+
+            switch (alias) {
+                case 'contents':
+                    this.showContents = true;
+                    this.disableContainerAdder();
+                    this.scrollToCurrent();
+                    break;
+                case 'blockadder':
+                    this.showBlockAdder = true;
+                    break;
+            }
+
+            if (this.selectedToolbarItem !== alias) {
+                this.setToolbarItem(alias);
+            }
+        },
+        disableContainerAdder() {
+            if (this.containerAdder !== false) {
+                this.coursewareContainerAdder(false);
+            }
+        },
+        scrollToCurrent() {
+            setTimeout(() => {
+                let contents = this.$refs.contents.$el; 
+                let current = contents.querySelector('.cw-tree-item-link-current');
+                if (current) {
+                    contents.scroll({ top: current.offsetTop - 4, behavior: 'smooth' });
+                }
+            }, 360);
+        },
+    },
+    mounted () {
+        this.selectTool(this.selectedToolbarItem);
+    },
+    watch: {
+        adderStorage(newValue) {
+            if (Object.keys(newValue).length !== 0) {
+                this.selectTool('blockadder');
+            }
+        },
+        consumeMode(newValue) {
+            if (newValue) {
+                this.selectTool('contents');
+            }
+        },
+        containerAdder(newValue) {
+            if (newValue === true) {
+                this.selectTool('blockadder');
+            }
+        },
+        showEditMode(newValue) {
+            if (!newValue) {
+                this.selectTool('contents');
+            }
+        },
+        toolsActive(newValue) {
+            const focusElement = this.$refs.tabs.getTabButtonByAlias(this.selectedToolbarItem);
+            if (newValue && focusElement) {
+                setTimeout(() => {
+                    this.initialFocusElement = focusElement;
+                    this.trap = true;
+                }, 300);
+            }
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue b/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
new file mode 100644
index 0000000..6efbca2
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
@@ -0,0 +1,80 @@
+<template>
+    <div role="region" id="search" aria-live="polite">
+        <courseware-ribbon
+            :showToolbarButton="false"
+            :showModeSwitchButton="false"
+            buttonsClass="single-icon"
+        >
+            <template #buttons>
+                <studip-icon shape="search" :size="24" />
+            </template>
+            <template #breadcrumbList>
+                <translate>Suchergebnisse</translate>
+            </template>
+            <template #menu>
+                <button :title="$gettext('Suchergebnisse schließen')" @click="closeResults">
+                    <studip-icon shape="decline" :size="24"/>
+                </button>
+            </template>
+        </courseware-ribbon>
+        <div id="search-results">
+            <article v-if="searchResults.length > 0">
+                <section v-for="result in searchResults" :key="result['structural-element-id']">
+                    <router-link
+                        :to="'/structural_element/' + result['structural-element-id']"
+                        @click.native="closeResults"
+                    >
+                        <div v-show="result.img !== null" class="search-result-img hidden-tiny-down">
+                            <img :src="result.img" />
+                        </div>
+                        <div class="search-result-data">
+                            <div class="search-result-title" v-html="result.name"></div>
+                            <div class="search-result-details">
+                                <div class="search-result-description" v-html="result.description"></div>
+                            </div>
+                        </div>
+                        <div class="search-result-information">
+                            <div class="search-result-time" v-html="result.date"></div>
+                        </div>
+                    </router-link>
+                </section>
+            </article>
+            <courseware-companion-box
+                v-else
+                :msgCompanion="$gettext('Es wurden keine Suchergebnisse gefunden.')"
+                mood="sad"
+            />
+        </div>
+    </div>
+</template>
+
+<script>
+import CoursewareRibbon from './CoursewareRibbon.vue';
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import StudipIcon from '../../StudipIcon.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-search-results',
+    components: {
+        CoursewareRibbon,
+        CoursewareCompanionBox,
+        StudipIcon
+    },
+    computed: {
+        ...mapGetters({
+            searchResults: 'searchResults'
+        }),
+    },
+    methods: {
+        ...mapActions({
+            setShowSearchResults: 'setShowSearchResults',
+            setSearchResults: 'setSearchResults',
+        }),
+        closeResults() {
+            this.setShowSearchResults(false);
+            this.setSearchResults([]);
+        },
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
new file mode 100644
index 0000000..dba1917
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
@@ -0,0 +1,1743 @@
+<template>
+    <focus-trap v-model="consumModeTrap">
+        <div>
+            <div
+                :class="{ 'cw-structural-element-consumemode': consumeMode }"
+                class="cw-structural-element"
+                v-if="validContext"
+            >
+                <div class="cw-structural-element-content" v-if="structuralElement">
+                    <courseware-ribbon :canEdit="canEdit && canAddElements" :isContentBar="true" @blockAdded="updateContainerList">
+                        <template #buttons>
+                            <router-link v-if="prevElement" :to="'/structural_element/' + prevElement.id">
+                                <div class="cw-ribbon-button cw-ribbon-button-prev" :title="textRibbon.perv" />
+                            </router-link>
+                            <div v-else class="cw-ribbon-button cw-ribbon-button-prev-disabled" :title="$gettext('Keine vorherige Seite')"/>
+                            <router-link v-if="nextElement" :to="'/structural_element/' + nextElement.id">
+                                <div class="cw-ribbon-button cw-ribbon-button-next" :title="textRibbon.next" />
+                            </router-link>
+                            <div v-else class="cw-ribbon-button cw-ribbon-button-next-disabled" :title="$gettext('Keine nächste Seite')"/>
+                        </template>
+                        <template #breadcrumbList>
+                            <li
+                                v-for="ancestor in ancestors"
+                                :key="ancestor.id"
+                                :title="ancestor.attributes.title"
+                                class="cw-ribbon-breadcrumb-item"
+                            >
+                                <span>
+                                    <router-link :to="'/structural_element/' + ancestor.id">{{ ancestor.attributes.title || "–" }}</router-link>
+                                </span>
+                            </li>
+                            <li
+                                class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current"
+                                :title="structuralElement.attributes.title"
+                            >
+                                <span>{{ structuralElement.attributes.title || "–" }}</span>
+                                <span v-if="isTask">[ {{ solverName }} ]</span>
+                                <template v-if="!userIsTeacher && inCourse">
+                                    <studip-icon
+                                        v-if="complete"
+                                        shape="accept"
+                                        role="info"
+                                        :title="$gettext('Diese Seite wurde von Ihnen vollständig bearbeitet')"
+                                    />
+                                    <span
+                                        v-else
+                                        :title="$gettextInterpolate(
+                                                    $gettext('Fortschritt: %{progress} %'),
+                                                    {progress: elementProgress}
+                                                )"
+                                    >
+                                        ({{ elementProgress }} %)
+                                    </span>
+                                </template>
+                            </li>
+                        </template>
+                        <template #breadcrumbFallback>
+                            <li
+                                class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current"
+                                :title="structuralElement.attributes.title"
+                            >
+                                <span>{{ structuralElement.attributes.title }}</span>
+                            </li>
+                        </template>
+                        <template #menu>
+                            <studip-action-menu
+                                v-if="!consumeMode"
+                                :items="menuItems"
+                                class="cw-ribbon-action-menu"
+                                :context="structuralElement.attributes.title"
+                                @editCurrentElement="menuAction('editCurrentElement')"
+                                @addElement="menuAction('addElement')"
+                                @deleteCurrentElement="menuAction('deleteCurrentElement')"
+                                @showInfo="menuAction('showInfo')"
+                                @showExportOptions="menuAction('showExportOptions')"
+                                @oerCurrentElement="menuAction('oerCurrentElement')"
+                                @setBookmark="menuAction('setBookmark')"
+                                @sortContainers="menuAction('sortContainers')"
+                                @pdfExport="menuAction('pdfExport')"
+                                @showSuggest="menuAction('showSuggest')"
+                                @linkElement="menuAction('linkElement')"
+                                @removeLock="menuAction('removeLock')"
+                                @activateFullscreen="menuAction('activateFullscreen')"
+                            />
+                        </template>
+                    </courseware-ribbon>
+
+                    <div v-if="structuralElementLoaded && !isLink" class="cw-companion-box-wrapper">
+                        <courseware-companion-box
+                            v-if="!canVisit"
+                            mood="sad"
+                            :msgCompanion="$gettext('Diese Seite steht Ihnen leider nicht zur Verfügung.')"
+                        />
+                        <courseware-companion-box
+                            v-if="blockedByAnotherUser"
+                            :msgCompanion="$gettextInterpolate($gettext('Die Einstellungen dieser Seite werden im Moment von %{blockingUserName} bearbeitet'), {blockingUserName: blockingUserName})"
+                            mood="pointing"
+                        >
+                            <template #companionActions>
+                                <button v-if="userIsTeacher" class="button" @click="menuAction('removeLock')">
+                                    {{ textRemoveLock.title }}
+                                </button>
+                            </template>
+                        </courseware-companion-box>
+                        <courseware-empty-element-box
+                            v-if="showEmptyElementBox"
+                            :canEdit="canEdit"
+                            :noContainers="noContainers"
+                        />
+                        <courseware-welcome-screen v-if="noContainers && isRoot && canEdit" />
+                    </div>
+
+                    <div
+                        v-if="canVisit && !editView && !isLink"
+                        class="cw-container-wrapper"
+                        :class="{
+                            'cw-container-wrapper-consume': consumeMode,
+                            'cw-container-wrapper-discuss': discussView,
+                        }"
+                    >
+                        <courseware-structural-element-discussion
+                            v-if="!noContainers && discussView"
+                            :structuralElement="structuralElement"
+                            :canEdit="canEdit"
+                        />
+                        <component
+                            v-for="container in containers"
+                            :key="container.id"
+                            :is="containerComponent(container)"
+                            :container="container"
+                            :canEdit="canEdit"
+                            :canAddElements="canAddElements"
+                            :isTeacher="userIsTeacher"
+                            class="cw-container-item"
+                        />
+                    </div>
+                    <div
+                        v-if="isLink"
+                        class="cw-container-wrapper"
+                        :class="{
+                            'cw-container-wrapper-consume': consumeMode,
+                            'cw-container-wrapper-discuss': discussView,
+                        }"
+                    >
+                        <courseware-structural-element-discussion
+                            v-if="discussView"
+                            :structuralElement="structuralElement"
+                            :canEdit="canEdit"
+                        />
+                        <div v-if="editView" class="cw-companion-box-wrapper">
+                            <courseware-companion-box
+                                :msgCompanion="$gettextInterpolate($gettext('Dieser Inhalt ist aus den persönlichen Lernmaterialien von %{ ownerName } verlinkt und kann nur dort bearbeitet werden.'), { ownerName: ownerName })"
+                                mood="pointing"
+                            />
+                        </div>
+                        <component
+                            v-for="container in linkedContainers"
+                            :key="container.id"
+                            :is="containerComponent(container)"
+                            :container="container"
+                            :canEdit="false"
+                            :canAddElements="false"
+                            :isTeacher="userIsTeacher"
+                            class="cw-container-item"
+                        />
+                    </div>
+                    <div v-if="canVisit && canEdit && editView && !isLink" class="cw-container-wrapper cw-container-wrapper-edit">
+                        <template v-if="!processing">
+                            <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
+                            <span id="operation" class="assistive-text">
+                                {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
+                            </span>
+                            <draggable
+                                class="cw-structural-element-list"
+                                tag="ol"
+                                role="listbox"
+                                v-model="containerList"
+                                v-bind="dragOptions"
+                                handle=".cw-sortable-handle"
+                                @start="isDragging = true"
+                                @end="dropContainer"
+                            >
+                                <li
+                                    v-for="container in containerList"
+                                    :key="container.id"
+                                    class="cw-container-item-sortable"
+                                >
+                                    <span
+                                        :class="{ 'cw-sortable-handle-dragging': isDragging }"
+                                        class="cw-sortable-handle"
+                                        tabindex="0"
+                                        role="button"
+                                        aria-describedby="operation"
+                                        :ref="'sortableHandle' + container.id"
+                                        @keydown="keyHandler($event, container.id)"
+                                    ></span>
+                                    <component
+                                        :is="containerComponent(container)"
+                                        :container="container"
+                                        :canEdit="canEdit"
+                                        :canAddElements="canAddElements"
+                                        :isTeacher="userIsTeacher"
+                                        class="cw-container-item"
+                                        ref="containers"
+                                        :class="{ 'cw-container-item-selected': keyboardSelected === container.id}"
+                                    />
+                                </li>
+                            </draggable>
+                        </template>
+                        <studip-progress-indicator v-if="processing" :description="$gettext('Vorgang wird bearbeitet...')" />
+                    </div>
+                </div>
+
+                <studip-dialog
+                    v-if="showEditDialog"
+                    :title="textEdit.title"
+                    :confirmText="textEdit.confirm"
+                    confirmClass="accept"
+                    :closeText="textEdit.close"
+                    closeClass="cancel"
+                    height="500"
+                    :width="inContent ? '720' : '500'"
+                    class="studip-dialog-with-tab"
+                    @close="closeEditDialog"
+                    @confirm="storeCurrentElement"
+                >
+                    <template v-slot:dialogContent>
+                        <courseware-tabs class="cw-tab-in-dialog">
+                            <courseware-tab :name="textEdit.basic" :selected="true" :index="0">
+                                <form class="default" @submit.prevent="">
+                                    <label>
+                                        <translate>Titel</translate>
+                                        <input type="text" v-model="currentElement.attributes.title" />
+                                    </label>
+                                    <label>
+                                        <translate>Beschreibung</translate>
+                                        <textarea
+                                            v-model="currentElement.attributes.payload.description"
+                                            class="cw-structural-element-description"
+                                        />
+                                    </label>
+                                </form>
+                            </courseware-tab>
+                            <courseware-tab :name="textEdit.meta" :index="1">
+                                <form class="default" @submit.prevent="">
+                                    <label>
+                                        <translate>Farbe</translate>
+                                        <studip-select
+                                            v-model="currentElement.attributes.payload.color"
+                                            :options="colors"
+                                            :reduce="(color) => color.class"
+                                            label="class"
+                                            class="cw-vs-select"
+                                        >
+                                            <template #open-indicator="selectAttributes">
+                                                <span v-bind="selectAttributes"
+                                                    ><studip-icon shape="arr_1down" :size="10"
+                                                /></span>
+                                            </template>
+                                            <template #no-options>
+                                                <translate>Es steht keine Auswahl zur Verfügung</translate>.
+                                            </template>
+                                            <template #selected-option="{ name, hex }">
+                                                <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                                                ><span>{{ name }}</span>
+                                            </template>
+                                            <template #option="{ name, hex }">
+                                                <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                                                ><span>{{ name }}</span>
+                                            </template>
+                                        </studip-select>
+                                    </label>
+                                    <label>
+                                        <translate>Art des Lernmaterials</translate>
+                                        <select v-model="currentElement.attributes.purpose">
+                                            <option value="content"><translate>Inhalt</translate></option>
+                                            <option v-if="!inCourse"  value="template"><translate>Aufgabenvorlage</translate></option>
+                                            <option value="oer"><translate>OER-Material</translate></option>
+                                            <option value="portfolio"><translate>ePortfolio</translate></option>
+                                            <option value="draft"><translate>Entwurf</translate></option>
+                                            <option value="other"><translate>Sonstiges</translate></option>
+                                        </select>
+                                    </label>
+                                    <label>
+                                        <translate>Lizenztyp</translate>
+                                        <select v-model="currentElement.attributes.payload.license_type">
+                                            <option v-for="license in licenses" :key="license.id" :value="license.id">
+                                                {{ license.name }}
+                                            </option>
+                                        </select>
+                                    </label>
+                                    <label>
+                                        <translate>Geschätzter zeitlicher Aufwand</translate>
+                                        <input type="text" v-model="currentElement.attributes.payload.required_time" />
+                                    </label>
+                                    <label>
+                                        <translate>Niveau</translate><br />
+                                        <translate>von</translate>
+                                        <select v-model="currentElement.attributes.payload.difficulty_start">
+                                            <option
+                                                v-for="difficulty_start in 12"
+                                                :key="difficulty_start"
+                                                :value="difficulty_start"
+                                            >
+                                                {{ difficulty_start }}
+                                            </option>
+                                        </select>
+                                        <translate>bis</translate>
+                                        <select v-model="currentElement.attributes.payload.difficulty_end">
+                                            <option
+                                                v-for="difficulty_end in 12"
+                                                :key="difficulty_end"
+                                                :value="difficulty_end"
+                                            >
+                                                {{ difficulty_end }}
+                                            </option>
+                                        </select>
+                                    </label>
+                                </form>
+                            </courseware-tab>
+                            <courseware-tab :name="textEdit.image" :index="2">
+                                <form class="default" @submit.prevent="">
+                                    <template v-if="hasImage">
+                                        <img
+                                            :src="image"
+                                            class="cw-structural-element-image-preview"
+                                            :alt="$gettext('Vorschaubild')"
+                                            />
+                                        <label>
+                                            <button class="button" @click="deleteImage" v-translate>Bild löschen</button>
+                                        </label>
+                                    </template>
+
+                                    <div v-else class="cw-structural-element-image-preview-placeholder"></div>
+
+                                    <div v-if="uploadFileError" class="messagebox messagebox_error">
+                                        {{ uploadFileError }}
+                                    </div>
+
+                                    <div v-show="!hasImage">
+                                        <label>
+                                            {{ $gettext('Bild hochladen') }}
+                                            <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
+                                        </label>
+                                        {{ $gettext('oder') }}
+                                        <br>
+                                        <button class="button" type="button" @click="showStockImageSelector = true">
+                                            {{ $gettext('Aus dem Bilderpool auswählen') }}
+                                        </button>
+                                        <StockImageSelector v-if="showStockImageSelector" @close="showStockImageSelector = false" @select="onSelectStockImage" />
+                                    </div>
+                                </form>
+                            </courseware-tab>
+                            <courseware-tab v-if="(inCourse && !isTask) || inContent" :name="textEdit.approval" :index="3">
+                                <courseware-structural-element-permissions
+                                    v-if="inCourse"
+                                    :element="currentElement"
+                                    @updateReadApproval="updateReadApproval"
+                                    @updateWriteApproval="updateWriteApproval"
+                                />
+                                <courseware-content-permissions
+                                    v-if="inContent"
+                                    :element="currentElement"
+                                    @updateReadApproval="updateReadApproval"
+                                    @updateWriteApproval="updateWriteApproval"
+                                />
+                            </courseware-tab>
+                            <courseware-tab v-if="inCourse && !isTask" :name="textEdit.visible" :index="4">
+                                <form class="default" @submit.prevent="">
+                                    <label>
+                                        <translate>Sichtbar ab</translate>
+                                        <input type="date" v-model="currentElement.attributes['release-date']" />
+                                    </label>
+                                    <label>
+                                        <translate>Unsichtbar ab</translate>
+                                        <input type="date" v-model="currentElement.attributes['withdraw-date']" />
+                                    </label>
+                                </form>
+                            </courseware-tab>
+                        </courseware-tabs>
+                    </template>
+                </studip-dialog>
+                <courseware-structural-element-dialog-add
+                    v-if="showAddDialog"
+                    :structuralElement="structuralElement"
+                    :isRoot="isRoot"
+                    :canEditParent="canEditParent"
+                />
+                <studip-dialog
+                    v-if="showInfoDialog"
+                    :title="textInfo.title"
+                    :closeText="textInfo.close"
+                    closeClass="cancel"
+                    @close="showElementInfoDialog(false)"
+                >
+                    <template v-slot:dialogContent>
+                        <table class="cw-structural-element-info">
+                            <tr>
+                                <td><translate>Titel</translate>:</td>
+                                <td>{{ structuralElement.attributes.title }}</td>
+                            </tr>
+                            <tr>
+                                <td><translate>Beschreibung</translate>:</td>
+                                <td>{{ structuralElement.attributes.payload.description }}</td>
+                            </tr>
+                            <tr>
+                                <td><translate>Seite wurde erstellt von</translate>:</td>
+                                <td>{{ ownerName }}</td>
+                            </tr>
+                            <tr>
+                                <td><translate>Seite wurde erstellt am</translate>:</td>
+                                <td><iso-date :date="structuralElement.attributes.mkdate" /></td>
+                            </tr>
+                            <tr>
+                                <td><translate>Zuletzt bearbeitet von</translate>:</td>
+                                <td>{{ editorName }}</td>
+                            </tr>
+                            <tr>
+                                <td><translate>Zuletzt bearbeitet am</translate>:</td>
+                                <td><iso-date :date="structuralElement.attributes.chdate" /></td>
+                            </tr>
+                        </table>
+                    </template>
+                </studip-dialog>
+
+                <studip-dialog
+                    v-if="showExportDialog"
+                    :title="textExport.title"
+                    :confirmText="textExport.confirm"
+                    confirmClass="accept"
+                    :closeText="textExport.close"
+                    closeClass="cancel"
+                    height="350"
+                    @close="showElementExportDialog(false)"
+                    @confirm="exportCurrentElement"
+                >
+                    <template v-slot:dialogContent>
+                        <div v-show="!exportRunning">
+                            <span v-translate>Hiermit exportieren Sie die Seite "%{ currentElement.attributes.title }" als ZIP-Datei.</span>
+                            <div class="cw-element-export">
+                                <label>
+                                    <input type="checkbox" v-model="exportChildren" />
+                                    <translate>Unterseiten exportieren</translate>
+                                </label>
+                            </div>
+                        </div>
+
+                        <courseware-companion-box
+                            v-show="exportRunning"
+                            :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')"
+                            mood="pointing"
+                        />
+                        <div v-show="exportRunning" class="cw-import-zip">
+                            <header>{{ exportState }}:</header>
+                            <div class="progress-bar-wrapper">
+                                <div
+                                    class="progress-bar"
+                                    role="progressbar"
+                                    :style="{ width: exportProgress + '%' }"
+                                    :aria-valuenow="exportProgress"
+                                    aria-valuemin="0"
+                                    aria-valuemax="100"
+                                >
+                                    {{ exportProgress }}%
+                                </div>
+                            </div>
+                        </div>
+                    </template>
+                </studip-dialog>
+
+                <studip-dialog
+                    v-if="showPdfExportDialog"
+                    :title="textExport.title"
+                    :confirmText="textExport.confirm"
+                    confirmClass="accept"
+                    :closeText="textExport.close"
+                    closeClass="cancel"
+                    height="350"
+                    @close="showElementPdfExportDialog(false)"
+                    @confirm="pdfExportCurrentElement"
+                >
+                    <template v-slot:dialogContent>
+                        <span v-translate>Hiermit exportieren Sie die Seite "%{ currentElement.attributes.title }" als PDF-Datei.</span>
+                            <div class="cw-element-export">
+                                <label>
+                                    <input type="checkbox" v-model="pdfExportChildren" />
+                                    <translate>Unterseiten exportieren</translate>
+                                </label>
+                            </div>
+                    </template>
+                </studip-dialog>
+
+                <studip-dialog
+                    v-if="showOerDialog"
+                    height="600"
+                    width="600"
+                    :title="textOer.title"
+                    :confirmText="textOer.confirm"
+                    confirmClass="accept"
+                    :closeText="textOer.close"
+                    closeClass="cancel"
+                    @close="showElementOerDialog(false)"
+                    @confirm="publishCurrentElement"
+                >
+                    <template v-slot:dialogContent>
+                        <form v-show="!oerExportRunning" class="default" @submit.prevent="">
+                            <fieldset>
+                                <legend><translate>Grunddaten</translate></legend>
+                                <label>
+                                    <p><translate>Vorschaubild</translate>:</p>
+                                    <img
+                                        v-if="currentElement.relationships.image.data"
+                                        :src="currentElement.relationships.image.meta['download-url']"
+                                        width="400"
+                                    />
+                                </label>
+                                <label>
+                                    <p><translate>Beschreibung</translate>:</p>
+                                    <p>{{ currentElement.attributes.payload.description }}</p>
+                                </label>
+                                <label>
+                                    <translate>Niveau</translate>:
+                                    <p>
+                                        {{ currentElement.attributes.payload.difficulty_start }} -
+                                        {{ currentElement.attributes.payload.difficulty_end }}
+                                    </p>
+                                </label>
+                                <label>
+                                    <translate>Lizenztyp</translate>:
+                                    <p>{{ currentLicenseName }}</p>
+                                </label>
+                                <label>
+                                    <translate>Sie können diese Daten unter "Seiteneinstellungen" verändern.</translate>
+                                </label>
+                            </fieldset>
+                            <fieldset>
+                                <legend><translate>Einstellungen</translate></legend>
+                                <label>
+                                    <translate>Unterseiten veröffentlichen</translate>
+                                    <input type="checkbox" v-model="oerChildren" />
+                                </label>
+                            </fieldset>
+                        </form>
+                        <courseware-companion-box
+                            v-show="oerExportRunning"
+                            :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')"
+                            mood="pointing"
+                        />
+                    </template>
+                </studip-dialog>
+                <studip-dialog
+                    v-if="showSuggestOerDialog"
+                    height="600"
+                    width="600"
+                    :title="textSuggestOer.title"
+                    :confirmText="textSuggestOer.confirm"
+                    confirmClass="accept"
+                    :closeText="textSuggestOer.close"
+                    closeClass="cancel"
+                    @close="updateShowSuggestOerDialog(false)"
+                    @confirm="sendOerSuggestion"
+                >
+                    <template v-slot:dialogContent>
+                        <p v-translate>Das folgende Courseware-Material wird %{ ownerName }
+                            zur Veröffentlichung im OER Campus vorgeschlagen:</p>
+                        <table class="cw-structural-element-info">
+                            <tr>
+                                <td><translate>Titel</translate>:</td>
+                                <td>{{ structuralElement.attributes.title }}</td>
+                            </tr>
+                            <tr>
+                                <td><translate>Beschreibung</translate>:</td>
+                                <td>{{ structuralElement.attributes.payload.description }}</td>
+                            </tr>
+                        </table>
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                <translate>Ihr Vorschlag wird anonym versendet. Falls gewünscht, können Sie
+                                    zusätzlich eine Nachricht verfassen:</translate>
+                                <textarea
+                                    v-model="additionalText"
+                                    class="cw-structural-element-description"
+                                />
+                            </label>
+                        </form>
+                    </template>
+                </studip-dialog>
+                <studip-dialog
+                    v-if="showDeleteDialog"
+                    :title="textDelete.title"
+                    :question="textDelete.alert"
+                    height="200"
+                    @confirm="deleteCurrentElement"
+                    @close="closeDeleteDialog"
+                ></studip-dialog>
+                <studip-dialog
+                    v-if="showPublicLinkDialog && inContent"
+                    :title="$gettext('Öffentlichen Link für Seite erzeugen')"
+                    :confirmText="$gettext('Erstellen')"
+                    confirmClass="accept"
+                    :closeText="$gettext('Abbrechen')"
+                    closeClass="cancel"
+                    class="cw-structural-element-dialog"
+                    @close="closePublicLinkDialog"
+                    @confirm="createElementPublicLink"
+                >
+                    <template v-slot:dialogContent>
+                        <form class="default" @submit.prevent="">
+                            <label>
+                                <translate>Passwort</translate>
+                                <input type="password" v-model="publicLink.password" />
+                            </label>
+                            <label>
+                                <translate>Ablaufdatum</translate>
+                                <input v-model="publicLink['expire-date']" type="date" class="size-l" />
+                            </label>
+                        </form>
+                    </template>
+                </studip-dialog>
+                <studip-dialog
+                    v-if="showRemoveLockDialog"
+                    :title="textRemoveLock.title"
+                    :question="textRemoveLock.alert"
+                    height="200"
+                    width="450"
+                    @confirm="executeRemoveLock"
+                    @close="showElementRemoveLockDialog(false)"
+                ></studip-dialog>
+
+                <courseware-structural-element-dialog-import v-if="showImportDialog"/>
+                <courseware-structural-element-dialog-copy v-if="showCopyDialog" />
+                <courseware-structural-element-dialog-link v-if="showLinkDialog"/>
+            </div>
+            <div v-else>
+                <courseware-companion-box
+                    v-if="currentElement !== ''"
+                    :msgCompanion="textCompanionWrongContext"
+                    mood="sad"
+                >
+                    <template v-slot:companionActions >
+                        <a class="button" :href="unitRootUrl">{{ $gettext('Lernmaterial neu laden') }}</a>
+                        <a class="button" :href="shelfURL">{{ $gettext('Zurück zur Lernmaterialübersicht') }}</a>
+                    </template>
+                </courseware-companion-box>
+            </div>
+        </div>
+    </focus-trap>
+</template>
+
+<script>
+import ContainerComponents from '../containers/container-components.js';
+import StructuralElementComponents from './structural-element-components.js';
+import CoursewarePluginComponents from '../plugin-components.js';
+import CoursewareStructuralElementDialogAdd from './CoursewareStructuralElementDialogAdd.vue';
+import CoursewareStructuralElementDialogCopy from './CoursewareStructuralElementDialogCopy.vue';
+import CoursewareStructuralElementDialogImport from './CoursewareStructuralElementDialogImport.vue';
+import CoursewareStructuralElementDialogLink from './CoursewareStructuralElementDialogLink.vue';
+import CoursewareStructuralElementDiscussion from './CoursewareStructuralElementDiscussion.vue';
+import CoursewareStructuralElementPermissions from './CoursewareStructuralElementPermissions.vue';
+import CoursewareContentPermissions from '../CoursewareContentPermissions.vue';
+import CoursewareWelcomeScreen from './CoursewareWelcomeScreen.vue';
+import CoursewareExport from '@/vue/mixins/courseware/export.js';
+import CoursewareOerMessage from '@/vue/mixins/courseware/oermessage.js';
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import wizardMixin from '@/vue/mixins/courseware/wizard.js';
+import CoursewareDateInput from '../layouts/CoursewareDateInput.vue';
+import StockImageSelector from '../../stock-images/SelectorDialog.vue';
+import StudipDialog from '../../StudipDialog.vue';
+import draggable from 'vuedraggable';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-structural-element',
+    components: Object.assign(StructuralElementComponents, {
+        CoursewareStructuralElementDialogAdd,
+        CoursewareStructuralElementDialogCopy,
+        CoursewareStructuralElementDialogImport,
+        CoursewareStructuralElementDialogLink,
+        CoursewareStructuralElementDiscussion,
+        CoursewareStructuralElementPermissions,
+        CoursewareContentPermissions,
+        CoursewareWelcomeScreen,
+        CoursewareDateInput,
+        StockImageSelector,
+        StudipDialog,
+        draggable,
+    }),
+    props: ['canVisit', 'orderedStructuralElements', 'structuralElement'],
+
+    mixins: [CoursewareExport, CoursewareOerMessage, colorMixin, wizardMixin],
+
+    data() {
+        return {
+            currentElement: '',
+            uploadFileError: '',
+            textCompanionWrongContext: this.$gettext('Die angeforderte Seite ist nicht Teil dieser Courseware.'),
+            textEdit: {
+                title: this.$gettext('Seiteneinstellungen'),
+                confirm: this.$gettext('Speichern'),
+                close: this.$gettext('Schließen'),
+                basic: this.$gettext('Grunddaten'),
+                image: this.$gettext('Bild'),
+                meta: this.$gettext('Metadaten'),
+                approval: this.$gettext('Rechte'),
+                visible: this.$gettext('Sichtbarkeit'),
+            },
+            textInfo: {
+                title: this.$gettext('Informationen zur Seite'),
+                close: this.$gettext('Schließen'),
+            },
+            textExport: {
+                title: this.$gettext('Seite exportieren'),
+                confirm: this.$gettext('Exportieren'),
+                close: this.$gettext('Schließen'),
+            },
+            textAdd: {
+                title: this.$gettext('Seite hinzufügen'),
+                confirm: this.$gettext('Erstellen'),
+                close: this.$gettext('Schließen'),
+            },
+            textRibbon: {
+                perv: this.$gettext('zurück'),
+                next: this.$gettext('weiter'),
+            },
+            textRemoveLock: {
+                title: this.$gettext('Sperre aufheben'),
+                alert: this.$gettext('Möchten Sie die Sperre der Seite wirklich aufheben?'),
+            },
+            exportRunning: false,
+            exportChildren: false,
+            oerExportRunning: false,
+            oerChildren: true,
+            pdfExportChildren: false,
+            containerList: [],
+            isDragging: false,
+            dragOptions: {
+                animation: 0,
+                group: 'description',
+                disabled: false,
+                ghostClass: 'container-ghost',
+            },
+            errorEmptyChapterName: false,
+            consumModeTrap: false,
+            additionalText: '',
+
+            publicLink: {
+                passsword: '',
+                'expire-date': ''
+            },
+            deletingPreviewImage: false,
+            processing: false,
+            keyboardSelected: null,
+            assistiveLive: '',
+            uploadImageURL: null,
+            showStockImageSelector: false,
+            selectedStockImage: null,
+        };
+    },
+
+    computed: {
+        ...mapGetters({
+            courseware: 'courseware',
+            context: 'context',
+            consumeMode: 'consumeMode',
+            containerById: 'courseware-containers/byId',
+            relatedContainers: 'courseware-containers/related',
+            relatedStructuralElements: 'courseware-structural-elements/related',
+            relatedTaskGroups: 'courseware-task-groups/related',
+            relatedUsers: 'users/related',
+            structuralElementById: 'courseware-structural-elements/byId',
+            userIsTeacher: 'userIsTeacher',
+            pluginManager: 'pluginManager',
+            showEditDialog: 'showStructuralElementEditDialog',
+            showAddDialog: 'showStructuralElementAddDialog',
+            showImportDialog: 'showStructuralElementImportDialog',
+            showCopyDialog: 'showStructuralElementCopyDialog',
+            showLinkDialog: 'showStructuralElementLinkDialog',
+            showExportDialog: 'showStructuralElementExportDialog',
+            showPdfExportDialog: 'showStructuralElementPdfExportDialog',
+            showInfoDialog: 'showStructuralElementInfoDialog',
+            showDeleteDialog: 'showStructuralElementDeleteDialog',
+            showOerDialog: 'showStructuralElementOerDialog',
+            showSuggestOerDialog: 'showSuggestOerDialog',
+            showPublicLinkDialog: 'showStructuralElementPublicLinkDialog',
+            showRemoveLockDialog: 'showStructuralElementRemoveLockDialog',
+            oerCampusEnabled: 'oerCampusEnabled',
+            oerEnableSuggestions: 'oerEnableSuggestions',
+            licenses: 'licenses',
+            exportState: 'exportState',
+            exportProgress: 'exportProgress',
+            userId: 'userId',
+            viewMode: 'viewMode',
+            taskById: 'courseware-tasks/byId',
+            userById: 'users/byId',
+            lastCreatedElement: 'courseware-structural-elements/lastCreated',
+            groupById: 'status-groups/byId',
+
+            blocked: 'currentElementBlocked',
+            blockerId: 'currentElementBlockerId',
+            blockedByThisUser: 'currentElementBlockedByThisUser',
+            blockedByAnotherUser: 'currentElementBlockedByAnotherUser',
+            isLink: 'currentElementisLink',
+
+            templates: 'courseware-templates/all',
+            progressData: 'progresses',
+        }),
+
+        currentId() {
+            return this.structuralElement?.id;
+        },
+
+        textOer() {
+            return {
+                title: this.$gettext('Lerninhalte auf dem OER Campus veröffentlichen'),
+                confirm: this.$gettext('Veröffentlichen'),
+                close: this.$gettext('Abbrechen'),
+            };
+        },
+
+        textSuggestOer() {
+            return {
+                title: this.$gettext('Lerninhalt für den OER Campus vorschlagen'),
+                confirm: this.$gettext('Lerninhalt vorschlagen'),
+                close: this.$gettext('Abbrechen'),
+            };
+        },
+
+        inCourse() {
+            return this.context.type === 'courses';
+        },
+
+        inContent() {
+            // The rights tab in contents will be only visible to the owner.
+            return this.context.type === 'users' && this.userId === this.currentElement.relationships.user.data.id;
+        },
+
+        textDelete() {
+            let textDelete = {};
+            textDelete.title = this.$gettext('Seite unwiderruflich löschen');
+            textDelete.alert = this.$gettext('Möchten Sie die Seite wirklich löschen?');
+            if (this.structuralElementLoaded) {
+                textDelete.alert =
+                    this.$gettextInterpolate(
+                        this.$gettext('Möchten Sie die Seite %{ pageTitle } und alle ihre Unterseiten wirklich löschen?'),
+                        {pageTitle: this.structuralElement.attributes.title}
+                    );
+            }
+
+            return textDelete;
+        },
+
+        validContext() {
+            if (this.context.type === 'sharedusers') {
+                if (this.context.id === this.courseware.relationships.root.data.id) {
+                    return true;
+                }
+            }
+
+            if (this.context.type === 'public') {
+                return true;
+            }
+
+            if (this.context.unit !== this.currentElement.relationships?.unit?.data?.id) {
+                return false;
+            }
+
+            if (this.context.type === 'courses' && this.currentElement.relationships) {
+                if (
+                    this.currentElement.relationships.course &&
+                    this.context.id === this.currentElement.relationships.course.data.id
+                ) {
+                    return true;
+                }
+            }
+
+            if (this.context.type === 'users' && this.currentElement.relationships) {
+                if (
+                    this.currentElement.relationships.user &&
+                    this.context.id === this.currentElement.relationships.user.data.id
+                ) {
+                    return true;
+                }
+            }
+            
+
+            return false;
+        },
+
+        image() {
+            if (this.selectedStockImage) {
+                return this.selectedStockImage.attributes['download-urls'].small
+            }
+            if (this.uploadImageURL) {
+                return this.uploadImageURL;
+            }
+            return this.structuralElement.relationships?.image?.meta?.['download-url'] ?? null;
+        },
+
+        imageType() {
+            return this.structuralElement.relationships?.image?.data?.type ?? null;
+        },
+
+        hasImage() {
+            return (this.image || this.selectedStockImage ) && this.deletingPreviewImage === false;
+        },
+
+        structuralElementLoaded() {
+            return this.structuralElement !== null && this.structuralElement !== {};
+        },
+
+        ancestors() {
+            if (!this.structuralElement) {
+                return [];
+            }
+
+            const finder = (parent) => {
+                const parentId = parent.relationships?.parent?.data?.id;
+                if (!parentId) {
+                    return null;
+                }
+                const element = this.structuralElementById({ id: parentId });
+                if (!element) {
+                    console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`);
+                }
+
+                return element;
+            };
+
+            const visitAncestors = function* (node) {
+                const parent = finder(node);
+                if (parent) {
+                    yield parent;
+                    yield* visitAncestors(parent);
+                }
+            };
+
+            return [...visitAncestors(this.structuralElement)].reverse();
+        },
+        prevElement() {
+            const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id);
+            if (currentIndex <= 0) {
+                return null;
+            }
+            const previousId = this.orderedStructuralElements[currentIndex - 1];
+            const previous = this.structuralElementById({ id: previousId });
+
+            return previous;
+        },
+        nextElement() {
+            const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id);
+            const lastIndex = this.orderedStructuralElements.length - 1;
+            if (currentIndex === -1 || currentIndex === lastIndex) {
+                return null;
+            }
+            const nextId = this.orderedStructuralElements[currentIndex + 1];
+            const next = this.structuralElementById({ id: nextId });
+
+            return next;
+        },
+        empty() {
+            if (this.containers === null) {
+                return true;
+            } else {
+                return !this.containers.some((container) => container.relationships.blocks.data.length > 0);
+            }
+        },
+        containers() {
+            if (!this.structuralElement) {
+                return [];
+            }
+
+            return (
+                this.relatedContainers({
+                    parent: this.structuralElement,
+                    relationship: 'containers',
+                }) ?? []
+            );
+        },
+        noContainers() {
+            if (this.containers === null) {
+                return true;
+            } else {
+                return this.containers.length === 0;
+            }
+        },
+
+        canEdit() {
+            if (!this.structuralElement) {
+                return false;
+            }
+            return this.structuralElement.attributes['can-edit'];
+        },
+
+        canEditParent() {
+            if (this.isRoot) {
+                return false;
+            }
+            const parentId = this.structuralElement.relationships.parent.data.id;
+            const parent = this.structuralElementById({ id: parentId });
+
+            return parent.attributes['can-edit'];
+        },
+
+        isRoot() {
+            return this.structuralElement.relationships.parent.data === null;
+        },
+
+        editor() {
+            const editor = this.relatedUsers({
+                parent: this.structuralElement,
+                relationship: 'editor',
+            });
+
+            return editor ?? null;
+        },
+
+        editorName() {
+            return this.editor?.attributes['formatted-name'] ?? '?';
+        },
+
+        menuItems() {
+            let menu = [
+                { id: 4, label: this.$gettext('Informationen anzeigen'), icon: 'info', emit: 'showInfo' },
+                { id: 5, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' },
+            ];
+
+            if (this.oerEnableSuggestions) {
+                menu.push(
+                    { id: 6, label: this.$gettext('Seite für OER Campus vorschlagen'), icon: 'oer-campus',
+                        emit: 'showSuggest' }
+                );
+            }
+
+            if (!document.documentElement.classList.contains('responsive-display')) {
+                menu.push(
+                    { id: 7, label: this.$gettext('Als Vollbild anzeigen'), icon: 'screen-full',
+                        emit: 'activateFullscreen'},
+                );
+            }
+
+            if (this.canEdit) {
+                if (!this.blockedByAnotherUser) {
+                    menu.push({
+                        id: 1,
+                        label: this.$gettext('Seiteneinstellungen'),
+                        icon: 'settings',
+                        emit: 'editCurrentElement',
+                    });
+                }
+                if (this.blockedByAnotherUser && this.userIsTeacher) {
+                    menu.push({
+                        id: 1,
+                        label: this.textRemoveLock.title,
+                        icon: 'lock-unlocked',
+                        emit: 'removeLock',
+                    });
+                }
+                menu.push({ id: 3, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' });
+            }
+            if (this.context.type === 'users') {
+                menu.push({ id: 8, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' });
+            }
+            if (!this.isRoot && this.canEdit && !this.isTask && !this.blocked) {
+                menu.push({
+                    id: 8,
+                    label: this.$gettext('Seite löschen'),
+                    icon: 'trash',
+                    emit: 'deleteCurrentElement',
+                });
+            }
+            menu.sort((a, b) => a.id - b.id);
+
+            return menu;
+        },
+        colors() {
+            return this.mixinColors.filter(color => color.darkmode);
+        },
+        currentLicenseName() {
+            for (let i = 0; i < this.licenses.length; i++) {
+                if (this.licenses[i]['id'] == this.currentElement.attributes.payload.license_type) {
+                    return this.licenses[i]['name'];
+                }
+            }
+
+            return '';
+        },
+        blockingUser() {
+            if (this.blockedByAnotherUser) {
+                return this.userById({id: this.blockerId});
+            }
+
+            return null;
+        },
+        blockingUserName() {
+            return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : '';
+        },
+        discussView() {
+            return this.viewMode === 'discuss';
+        },
+        editView() {
+            return this.viewMode === 'edit';
+        },
+        pdfExportURL() {
+            if (this.context.type === 'users') {
+                return STUDIP.URLHelper.getURL(
+                    'dispatch.php/contents/courseware/pdf_export/' + this.structuralElement.id
+                );
+            }
+            if (this.context.type === 'courses') {
+                return STUDIP.URLHelper.getURL(
+                    'dispatch.php/course/courseware/pdf_export/' + this.structuralElement.id
+                );
+            }
+
+            return '';
+        },
+        isTask() {
+            return this.structuralElement?.relationships.task.data !== null;
+        },
+        task() {
+            if (!this.isTask) {
+                return null;
+            }
+
+            return this.taskById({ id: this.structuralElement.relationships.task.data.id });
+        },
+        solver() {
+            if (this.task) {
+                const solver = this.task.relationships.solver.data;
+                if (solver.type === 'users') {
+                    return this.userById({ id: solver.id });
+                }
+                if (solver.type === 'status-groups') {
+                    return this.groupById({ id: solver.id });
+                }
+            }
+
+            return null;
+        },
+        solverName() {
+            if (this.solver) {
+                if (this.solver.type === 'users') {
+                    return this.solver.attributes['formatted-name'];
+                }
+                if (this.solver.type === 'status-groups') {
+                    return this.solver.attributes.name;
+                }
+            }
+
+            return '';
+        },
+        canAddElements() {
+            if (!this.isTask) {
+                return true;
+            }
+
+            // still loading
+            if (!this.task) {
+                return false;
+            }
+
+            const taskGroup = this.relatedTaskGroups({ parent: this.task, relationship: 'task-group' });
+
+            return taskGroup?.attributes['solver-may-add-blocks'];
+        },
+        showEmptyElementBox() {
+            if (!this.empty) {
+                return false;
+            }
+
+            return (
+                (!this.isRoot && this.canEdit) || !this.canEdit || (!this.noContainers && this.isRoot && this.canEdit)
+            );
+        },
+
+        linkedElement() {
+            if (this.isLink) {
+                return this.structuralElementById({ id: this.structuralElement.attributes['target-id']});
+            }
+
+            return null;
+        },
+
+        linkedContainers() {
+            let containers = [];
+            let relatedContainers = this.linkedElement?.relationships?.containers?.data;
+
+            if (relatedContainers) {
+                for (const container of relatedContainers) {
+                    containers.push(this.containerById({ id: container.id}));
+                }
+            }
+
+            return containers;
+        },
+
+        owner() {
+            const owner = this.relatedUsers({
+                parent: this.structuralElement,
+                relationship: 'owner',
+            });
+            return owner ?? null;
+        },
+
+        ownerName() {
+            return this.owner?.attributes['formatted-name'] ?? '?';
+        },
+        complete() {
+            return this.elementProgress === 100;
+        },
+        elementProgress() {
+            if (this.structuralElementLoaded) {
+                return this.progressData?.[this.structuralElement.id].progress.self;
+            }
+
+            return 0;
+        },
+        progressTitle() {
+            return '';
+        },
+        shelfURL() {
+            return STUDIP.URLHelper.getURL(
+                'dispatch.php/course/courseware/',
+                {cid: this.context.id}
+            );
+        },
+        unitRootUrl() {
+            return STUDIP.URLHelper.getURL(
+                'dispatch.php/course/courseware/courseware/' + this.context.unit,
+                {cid: this.context.id}
+            );
+        }
+    },
+
+    methods: {
+        ...mapActions({
+            updateStructuralElement: 'updateStructuralElement',
+            deleteStructuralElement: 'deleteStructuralElement',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+            addBookmark: 'addBookmark',
+            companionInfo: 'companionInfo',
+            companionWarning: 'companionWarning',
+            companionError: 'companionError',
+            uploadImageForStructuralElement: 'uploadImageForStructuralElement',
+            deleteImageForStructuralElement: 'deleteImageForStructuralElement',
+            companionSuccess: 'companionSuccess',
+            setStockImageForStructuralElement: 'setStockImageForStructuralElement',
+            showElementEditDialog: 'showElementEditDialog',
+            showElementAddDialog: 'showElementAddDialog',
+            showElementExportDialog: 'showElementExportDialog',
+            showElementPdfExportDialog: 'showElementPdfExportDialog',
+            showElementInfoDialog: 'showElementInfoDialog',
+            showElementDeleteDialog: 'showElementDeleteDialog',
+            showElementOerDialog: 'showElementOerDialog',
+            showElementPublicLinkDialog: 'showElementPublicLinkDialog',
+            showElementRemoveLockDialog: 'showElementRemoveLockDialog',
+            updateShowSuggestOerDialog: 'updateShowSuggestOerDialog',
+            updateContainer: 'updateContainer',
+            sortContainersInStructualElements: 'sortContainersInStructualElements',
+            loadTask: 'loadTask',
+            loadStructuralElement: 'loadStructuralElement',
+            createLink: 'createLink',
+            setCurrentElementId: 'coursewareCurrentElement',
+            loadProgresses: 'loadProgresses'
+        }),
+
+        initCurrent() {
+            this.currentElement = _.cloneDeep(this.structuralElement);
+            this.uploadFileError = '';
+            this.deletingPreviewImage = false;
+            this.uploadImageURL = null;
+        },
+        async menuAction(action) {
+            switch (action) {
+                case 'removeLock':
+                    this.displayRemoveLockDialog();
+                    break;
+                case 'editCurrentElement':
+                    await this.loadStructuralElement(this.currentId);
+                    if (this.blockedByAnotherUser) {
+                        this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+
+                        return false;
+                    }
+                    try {
+                        await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+                    } catch(error) {
+                        if (error.status === 409) {
+                            this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+                        } else {
+                            console.log(error);
+                        }
+
+                        return false;
+                    }
+                    this.initCurrent();
+                    this.showElementEditDialog(true);
+                    break;
+                case 'addElement':
+                    this.errorEmptyChapterName = false;
+                    this.showElementAddDialog(true);
+                    break;
+                case 'deleteCurrentElement':
+                    await this.loadStructuralElement(this.currentId);
+                    if (this.blockedByAnotherUser) {
+                        this.companionInfo({
+                            info: this.$gettextInterpolate(
+                                this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Seite bearbeitet.'),
+                                {blockingUserName: this.blockingUserName}
+                            )
+                        });
+
+                        return false;
+                    }
+                    await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+                    this.showElementDeleteDialog(true);
+                    break;
+                case 'showInfo':
+                    this.showElementInfoDialog(true);
+                    break;
+                case 'showExportOptions':
+                    this.showElementExportDialog(true);
+                    break;
+                case 'oerCurrentElement':
+                    this.showElementOerDialog(true);
+                    break;
+                case 'showSuggest':
+                    this.updateShowSuggestOerDialog(true);
+                    break;
+                case 'setBookmark':
+                    this.setBookmark();
+                    break;
+                case 'linkElement':
+                    this.showElementPublicLinkDialog(true);
+                    break;
+                case 'activateFullscreen':
+                    STUDIP.Fullscreen.activate();
+                    break;
+            }
+        },
+        async closeEditDialog() {
+            await this.loadStructuralElement(this.currentElement.id);
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+                await this.loadStructuralElement(this.currentElement.id);
+            }
+            this.showElementEditDialog(false);
+            this.initCurrent();
+        },
+        closeAddDialog() {
+            this.showElementAddDialog(false);
+        },
+        checkUploadFile() {
+            const file = this.$refs?.upload_image?.files[0];
+            this.uploadImageURL = null;
+            this.uploadFileError = this.checkUploadImageFile(this.$refs?.upload_image?.files[0]);
+            if (this.uploadFileError === '') {
+                this.deletingPreviewImage = false;
+                this.uploadImageURL = window.URL.createObjectURL(file);
+            }
+        },
+        deleteImage() {
+            if (!this.deletingPreviewImage) {
+                this.deletingPreviewImage = true;
+            }
+        },
+        async storeCurrentElement() {
+            await this.loadStructuralElement(this.currentElement.id);
+            if (this.blockedByAnotherUser) {
+                this.companionWarning({
+                    info: this.$gettextInterpolate(
+                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
+                        {blockingUserName: this.blockingUserName}
+                    )
+                });
+                this.showElementEditDialog(false);
+                return false;
+            }
+            if (!this.blocked) {
+                await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            }
+
+            const file = this.$refs?.upload_image?.files[0];
+            try {
+                this.uploadFileError = '';
+                if (file) {
+                    await this.uploadImageForStructuralElement({
+                        structuralElement: this.currentElement,
+                        file,
+                    });
+                } else if (this.selectedStockImage) {
+                    await this.setStockImageForStructuralElement({
+                        structuralElement: this.currentElement,
+                        stockImage: this.selectedStockImage,
+                    })
+                } else if (this.deletingPreviewImage) {
+                    await this.deleteImageForStructuralElement(this.currentElement);
+                }
+
+                this.loadStructuralElement(this.currentElement.id);
+            } catch(error) {
+                console.error(error);
+                this.uploadFileError = this.$gettext('Das Bild für das neue Lernmaterial konnte nicht gespeichert werden.');
+            }
+
+            this.showElementEditDialog(false);
+
+            if (this.currentElement.attributes['release-date'] !== '') {
+                this.currentElement.attributes['release-date'] =
+                    new Date(this.currentElement.attributes['release-date']).getTime() / 1000;
+            }
+
+            if (this.currentElement.attributes['withdraw-date'] !== '') {
+                this.currentElement.attributes['withdraw-date'] =
+                    new Date(this.currentElement.attributes['withdraw-date']).getTime() / 1000;
+            }
+
+            const element = {
+                id: this.currentElement.id,
+                type: this.currentElement.type,
+                attributes: this.currentElement.attributes,
+            };
+
+            await this.updateStructuralElement({ element, id: this.currentId});
+            await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            this.$emit('select', this.currentId);
+            this.initCurrent();
+        },
+
+        dropContainer() {
+            this.isDragging = false;
+            this.storeSort();
+        },
+
+        async storeSort() {
+            const timeout = setTimeout(() => this.processing = true, 800);
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+                clearTimeout(timeout);
+                this.processing = false;
+                return false;
+            }
+            try {
+                await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            } catch (error) {
+                if (error.status === 409) {
+                    this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+                } else {
+                    console.log(error);
+                }
+
+                clearTimeout(timeout);
+                this.processing = false;
+                return false;
+            }
+
+            await this.sortContainersInStructualElements({
+                structuralElement: this.structuralElement,
+                containers: this.containerList,
+            });
+            this.$emit('select', this.currentId);
+
+            clearTimeout(timeout);
+            this.processing = false;
+        },
+
+        async exportCurrentElement(data) {
+            if (this.exportRunning) {
+                return;
+            }
+
+            this.exportRunning = true;
+
+            await this.sendExportZip(this.currentElement.id, {
+                withChildren: this.exportChildren,
+            });
+
+            this.exportRunning = false;
+            this.showElementExportDialog(false);
+        },
+
+        pdfExportCurrentElement() {
+            this.showElementPdfExportDialog(false);
+            let url = '';
+            let withChildren = this.pdfExportChildren ? '/1' : '/0';
+            if (this.context.type === 'users') {
+                url = STUDIP.URLHelper.getURL('dispatch.php/contents/courseware/pdf_export/' + this.structuralElement.id + withChildren);
+            }
+            if (this.context.type === 'courses') {
+                url = STUDIP.URLHelper.getURL('dispatch.php/course/courseware/pdf_export/' + this.structuralElement.id + withChildren);
+            }
+
+            if (url) {
+                window.open(url , '_blank').focus();
+            }
+        },
+
+        async publishCurrentElement() {
+            if (this.oerExportRunning) {
+                return;
+            }
+            this.oerExportRunning = true;
+            await this.exportToOER(this.currentElement, { withChildren: this.oerChildren });
+            this.oerExportRunning = false;
+            this.showElementOerDialog(false);
+        },
+
+        async closeDeleteDialog() {
+            await this.loadStructuralElement(this.currentElement.id);
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            }
+            this.showElementDeleteDialog(false);
+        },
+        async deleteCurrentElement() {
+            await this.loadStructuralElement(this.currentElement.id);
+            if (this.blockedByAnotherUser) {
+                this.companionWarning({
+                    info: this.$gettextInterpolate(
+                        this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'),
+                        {blockingUserName: this.blockingUserName}
+                    )
+                });
+                this.showElementDeleteDialog(false);
+                return false;
+            }
+            let parent_id = this.structuralElement.relationships.parent.data.id;
+            this.showElementDeleteDialog(false);
+            this.companionInfo({ info: this.$gettext('Lösche Seite und alle darunter liegenden Elemente.') });
+            this.deleteStructuralElement({
+                id: this.currentId,
+                parentId: this.structuralElement.relationships.parent.data.id,
+            })
+            .then(response => {
+                this.$router.push(parent_id);
+                this.companionInfo({ info: this.$gettext('Die Seite wurde gelöscht.') });
+            })
+            .catch(error => {
+                this.companionError({ info: this.$gettext('Die Seite konnte nicht gelöscht werden.') });
+            });
+        },
+        containerComponent(container) {
+            return 'courseware-' + container.attributes['container-type'] + '-container';
+        },
+        setBookmark() {
+            this.addBookmark(this.structuralElement);
+            this.companionInfo({ info: this.$gettext('Das Lesezeichen wurde gesetzt.') });
+        },
+        updateReadApproval(approval) {
+            this.currentElement.attributes['read-approval'] = approval;
+        },
+        updateWriteApproval(approval) {
+            this.currentElement.attributes['write-approval'] = approval;
+        },
+        sendOerSuggestion() {
+            this.suggestViaAction(this.currentElement, this.additionalText);
+            this.updateShowSuggestOerDialog(false);
+        },
+        async createElementPublicLink() {
+            const date = this.publicLink['expire-date'];
+            const publicLink = {
+                attributes: {
+                    password: this.publicLink.password,
+                    'expire-date': date === '' ? new Date(0).toISOString() : new Date(date).toISOString()
+                },
+                relationships: {
+                    'structural-element': {
+                        data: {
+                            id: this.currentElement.id,
+                            type: 'courseware-structural-elements'
+                        }
+                    }
+                }
+            }
+
+            await this.createLink({ publicLink });
+            this.companionSuccess({
+                info: this.$gettext('Öffentlicher Link wurde angelegt. Unter Freigaben finden Sie alle Ihre öffentlichen Links.'),
+            });
+            this.closePublicLinkDialog();
+        },
+        closePublicLinkDialog() {
+            this.publicLink = {
+                passsword: '',
+                'expire-date': ''
+            };
+            this.showElementPublicLinkDialog(false);
+        },
+        displayRemoveLockDialog() {
+            this.showElementRemoveLockDialog(true);
+        },
+        async executeRemoveLock() {
+            await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            await this.loadStructuralElement(this.currentElement.id);
+            this.showElementRemoveLockDialog(false);
+        },
+        updateContainerList() {
+            this.containerList = this.containers;
+            const containerRefs = this.$refs.containers;
+            for (let ref of containerRefs) {
+                ref.initCurrentData();
+            }
+        },
+        keyHandler(e, containerId) {
+            switch (e.keyCode) {
+                case 27: // esc
+                    this.abortKeyboardSorting(containerId);
+                    break;
+                case 13: // enter
+                    e.preventDefault();
+                    if (this.keyboardSelected) {
+                        this.storeKeyboardSorting(containerId);
+                    } else {
+                        this.keyboardSelected = containerId;
+                        const container = this.containerById({id: containerId});
+                        const index = this.containerList.findIndex(c => c.id === container.id);
+                        this.assistiveLive =
+                            this.$gettextInterpolate(
+                                this.$gettext('%{containerTitle} Abschnitt ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
+                                , {containerTitle: container.attributes.title, pos: index + 1, listLength: this.containerList.length}
+                            );
+                    }
+                    break;
+            }
+            if (this.keyboardSelected) {
+                switch (e.keyCode) {
+                    case 9: //tab
+                        this.abortKeyboardSorting(containerId);
+                        break;
+                    case 38: // up
+                        e.preventDefault();
+                        this.moveItemUp(containerId);
+                        break;
+                    case 40: // down
+                        e.preventDefault();
+                        this.moveItemDown(containerId);
+                        break;
+                }
+            }
+        },
+        moveItemUp(containerId) {
+            const currentIndex = this.containerList.findIndex(container => container.id === containerId);
+            if (currentIndex !== 0) {
+                const container = this.containerById({id: containerId});
+                const newPos = currentIndex - 1;
+                this.containerList.splice(newPos, 0, this.containerList.splice(currentIndex, 1)[0]);
+                this.assistiveLive =
+                    this.$gettextInterpolate(
+                        this.$gettext('%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
+                        , {containerTitle: container.attributes.title, pos: newPos + 1, listLength: this.containerList.length}
+                    );
+            }
+        },
+        moveItemDown(containerId) {
+            const currentIndex = this.containerList.findIndex(container => container.id === containerId);
+            if (this.containerList.length - 1 > currentIndex) {
+                const container = this.containerById({id: containerId});
+                const newPos = currentIndex + 1;
+                this.containerList.splice(newPos, 0, this.containerList.splice(currentIndex, 1)[0]);
+                this.assistiveLive =
+                    this.$gettextInterpolate(
+                        this.$gettext('%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
+                        , {containerTitle: container.attributes.title, pos: newPos + 1, listLength: this.containerList.length}
+                    );
+            }
+        },
+        abortKeyboardSorting(containerId) {
+            const container = this.containerById({id: containerId});
+            this.keyboardSelected = null;
+            this.assistiveLive =
+                this.$gettextInterpolate(
+                    this.$gettext('%{containerTitle} Abschnitt, Neuordnung abgebrochen.')
+                    , {containerTitle: container.attributes.title}
+                );
+            this.$emit('select', this.currentId);
+        },
+        storeKeyboardSorting(containerId) {
+            const container = this.containerById({id: containerId});
+            const currentIndex = this.containerList.findIndex(container => container.id === containerId);
+            this.keyboardSelected = null;
+            this.assistiveLive =
+                this.$gettextInterpolate(
+                    this.$gettext('%{containerTitle} Abschnitt, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.')
+                    , {containerTitle: container.attributes.title, pos: currentIndex + 1, listLength: this.containerList.length}
+                );
+            this.storeSort();
+        },
+        onSelectStockImage(stockImage) {
+            if (this.$refs?.upload_image) {
+                this.$refs.upload_image.value = null;
+            }
+            this.selectedStockImage = stockImage;
+            this.showStockImageSelector = false;
+            this.deletingPreviewImage = false;
+        },
+    },
+    created() {
+        this.pluginManager.registerComponentsLocally(this);
+    },
+
+    watch: {
+        async structuralElement() {
+            this.setCurrentElementId(this.structuralElement.id);
+            this.initCurrent();
+            if (this.isTask) {
+                this.loadTask({
+                    taskId: this.structuralElement.relationships.task.data.id,
+                });
+            }
+
+            if (this.isLink) {
+                this.loadStructuralElement(this.structuralElement.attributes['target-id']);
+            }
+
+            if (this.inCourse && this.courseware.attributes['sequential-progression'] && !this.userIsTeacher) {
+               this.loadProgresses();
+            }
+        },
+        containers() {
+            this.containerList = this.containers;
+        },
+        containerList() {
+            if (this.keyboardSelected) {
+                this.$nextTick(() => {
+                    const selected = this.$refs['sortableHandle' + this.keyboardSelected][0];
+                    selected.focus();
+                    selected.scrollIntoView({behavior: "smooth", block: "center"});
+                });
+            }
+        },
+        consumeMode(newState) {
+            this.consumModeTrap = newState;
+        },
+    },
+
+    // this line provides all the components to courseware plugins
+    provide: () => ({
+        containerComponents: ContainerComponents,
+        coursewarePluginComponents: CoursewarePluginComponents,
+    }),
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue
new file mode 100644
index 0000000..42c7f5f
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue
@@ -0,0 +1,139 @@
+<template>
+    <section
+        class="cw-structural-element-comments"
+        :class="[emptyComments ? 'cw-structural-element-comments-empty' : '']"
+    >
+        <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
+        <div class="cw-structural-element-comments-items" v-show="!emptyComments" ref="commentsRef">
+            <courseware-talk-bubble
+                v-for="comment in comments"
+                :key="comment.id"
+                :payload="buildPayload(comment)"
+            />
+        </div>
+        <div class="cw-structural-element-comment-create">
+            <textarea v-model="createComment" :placeholder="placeHolder" spellcheck="true"></textarea>
+            <button class="button" @click="postComment"><translate>Senden</translate></button>
+        </div>
+    </section>
+</template>
+
+<script>
+import CoursewareTalkBubble from '../layouts/CoursewareTalkBubble.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-structural-element-comments',
+    components: {
+        CoursewareTalkBubble,
+    },
+    props: {
+        structuralElement: Object,
+    },
+    data() {
+        return {
+            createComment: '',
+            placeHolder: this.$gettext('Stellen Sie eine Frage oder kommentieren Sie...'),
+            srMessage: ''
+        };
+    },
+    computed: {
+        ...mapGetters({
+            relatedUser: 'users/related',
+            userId: 'userId',
+            getComments: 'courseware-structural-element-comments/related',
+        }),
+        comments() {
+            const parent = {
+                type: this.structuralElement.type,
+                id: this.structuralElement.id,
+            };
+
+            return this.getComments({ parent, relationship: 'comments' });
+        },
+        emptyComments() {
+            if (this.comments === null || this.comments.length === 0) {
+                return true;
+            }
+
+            return false;
+        }
+    },
+    methods: {
+        ...mapActions({
+            createComments: 'courseware-structural-element-comments/create',
+            loadRelatedComments: 'courseware-structural-element-comments/loadRelated'
+        }),
+        async loadComments() {
+            const parent = {
+                type: this.structuralElement.type,
+                id: this.structuralElement.id,
+            };
+            await this.loadRelatedComments({
+                parent,
+                relationship: 'comments',
+                options: {
+                    include: 'user',
+                },
+            });
+        },
+        async postComment() {
+            this.updateSrMessage(this.$gettext('Kommentar gesendet'));
+            const data = {
+                attributes: {
+                    comment: this.createComment
+                },
+                relationships: {
+                    'structural-element': {
+                        data: {
+                            id: this.structuralElement.id,
+                            type: this.structuralElement.type
+                        }
+                    }
+                }
+            };
+
+            await this.createComments(data);
+            this.loadComments();
+            this.createComment = '';
+        },
+        buildPayload(comment) {
+            const commenter = this.relatedUser({
+                parent: { id: comment.id, type: comment.type },
+                relationship: 'user',
+            });
+
+            const payload = {
+                id: comment.id,
+                own: comment.relationships.user.data.id === this.userId,
+                content: comment.attributes.comment,
+                chdate: comment.attributes.chdate,
+                mkdate: comment.attributes.mkdate,
+                user_id: commenter.id,
+                user_name: commenter.attributes['formatted-name'],
+                user_avatar: commenter.meta.avatar.small,
+            };
+
+            return payload;
+        },
+        updateSrMessage(message) {
+            this.srMessage = '';
+            this.srMessage = message;
+        }
+    },
+    mounted() {
+        this.loadComments();
+    },
+    updated() {
+        let ref = this.$refs["commentsRef"];
+        ref.scrollTop = ref.scrollHeight;
+    },
+    watch: {
+        comments() {
+            if (this.comments && this.comments.length > 0) {
+                this.$emit('hasComments');
+            }
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
new file mode 100644
index 0000000..9ba6c77
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
@@ -0,0 +1,380 @@
+<template>
+    <studip-wizard-dialog
+        :title="$gettext('Seite hinzufügen')"
+        :confirmText="$gettext('Erstellen')"
+        :closeText="$gettext('Abbrechen')"
+        :slots="wizardSlots"
+        :lastRequiredSlotId="1"
+        :requirements="requirements"
+        @close="closeAddDialog"
+        @confirm="createElement"
+    >
+        <template v-slot:basic>
+            <form class="default" @submit.prevent="">
+                <label>
+                    {{ $gettext('Position der neuen Seite') }}
+                    <select v-model="pageParent">
+                        <option v-if="!isRoot && canEditParent" value="sibling">
+                            {{ $gettext('Neben der aktuellen Seite') }}
+                        </option>
+                        <option value="descendant">{{ $gettext('Unterhalb der aktuellen Seite') }}</option>
+                    </select>
+                </label>
+                <label>
+                    <span>{{ text.title }}</span>
+                    <span aria-hidden="true" class="wizard-required">*</span>
+                    <input type="text" v-model="title" required />
+                </label>
+                <label>
+                    <span>{{ $gettext('Beschreibung') }}</span>
+                    <textarea v-model="description" required />
+                </label>
+            </form>
+        </template>
+        <template v-slot:template>
+            <form v-if="hasTemplates" class="default" @submit.prevent="">
+                <label>
+                    {{ $gettext('Art der Vorlage') }}
+                    <select v-model="templatePurpose">
+                        <option value="content">{{ $gettext('Inhalt') }}</option>
+                        <option value="oer">{{ $gettext('OER-Material') }}</option>
+                        <option value="portfolio">{{ $gettext('ePortfolio') }}</option>
+                        <option value="draft">{{ $gettext('Entwurf') }}</option>
+                        <option v-if="!inCourseContext" value="template">{{ $gettext('Aufgabenvorlage') }}</option>
+                        <option value="other">{{ $gettext('Sonstiges') }}</option>
+                    </select>
+                </label>
+                <label>
+                    <span>{{ $gettext('Vorlage') }}</span>
+                    <select v-model="selectedTemplate">
+                        <option :value="null">{{ $gettext('ohne Vorlage') }}</option>
+                        <option v-for="template in selectableTemplates" :key="template.id" :value="template">
+                            {{ template.attributes.name }}
+                        </option>
+                    </select>
+                </label>
+            </form>
+            <courseware-companion-box
+                v-else
+                :msgCompanion="$gettext('Es wurden keine Vorlagen gefunden.')"
+                mood="pointing"
+                class="cw-companion-box-in-form"
+            />
+        </template>
+        <template v-slot:layout>
+            <form class="default" @submit.prevent="">
+                <label>
+                    {{ $gettext('Bild') }}
+                    <input
+                        class="cw-file-input"
+                        ref="upload_image"
+                        type="file"
+                        accept="image/*"
+                        @change="checkUploadFile"
+                    />
+                    <courseware-companion-box
+                        v-if="uploadFileError"
+                        :msgCompanion="uploadFileError"
+                        mood="sad"
+                        class="cw-companion-box-in-form"
+                    />
+                </label>
+                <label>
+                    {{ $gettext('Farbe') }}
+                    <studip-select v-model="color" :options="colors" :reduce="(color) => color.class" label="class">
+                        <template #open-indicator="selectAttributes">
+                            <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span>
+                        </template>
+                        <template #no-options>
+                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                        </template>
+                        <template #selected-option="{ name, hex }">
+                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span>
+                            <span>{{ name }}</span>
+                        </template>
+                        <template #option="{ name, hex }">
+                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span>
+                            <span>{{ name }}</span>
+                        </template>
+                    </studip-select>
+                </label>
+            </form>
+        </template>
+        <template v-slot:advanced>
+            <form class="default" @submit.prevent="">
+                <label>
+                    {{ $gettext('Art des Lernmaterials') }}
+                    <select v-model="purpose">
+                        <option value="content">{{ $gettext('Inhalt') }}</option>
+                        <option value="oer">{{ $gettext('OER-Material') }}</option>
+                        <option value="portfolio">{{ $gettext('ePortfolio') }}</option>
+                        <option value="draft">{{ $gettext('Entwurf') }}</option>
+                        <option v-if="!inCourseContext" value="template">{{ $gettext('Aufgabenvorlage') }}</option>
+                        <option value="other">{{ $gettext('Sonstiges') }}</option>
+                    </select>
+                </label>
+                <label>
+                    {{ $gettext('Lizenztyp') }}
+                    <select v-model="license_type">
+                        <option v-for="license in licenses" :key="license.id" :value="license.id">
+                            {{ license.name }}
+                        </option>
+                    </select>
+                </label>
+                <label>
+                    {{ $gettext('Geschätzter zeitlicher Aufwand') }}
+                    <input type="text" v-model="required_time" />
+                </label>
+                <label>
+                    {{ $gettext('Niveau') }}<br />
+                    {{ $gettext('von') }}
+                    <select v-model="difficulty_start">
+                        <option v-for="difficulty_start in 12" :key="difficulty_start" :value="difficulty_start">
+                            {{ difficulty_start }}
+                        </option>
+                    </select>
+                    {{ $gettext('bis') }}
+                    <select v-model="difficulty_end">
+                        <option v-for="difficulty_end in 12" :key="difficulty_end" :value="difficulty_end">
+                            {{ difficulty_end }}
+                        </option>
+                    </select>
+                </label>
+            </form>
+        </template>
+    </studip-wizard-dialog>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import StudipSelect from '../../StudipSelect.vue';
+import StudipWizardDialog from '../../StudipWizardDialog.vue';
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import wizardMixin from '@/vue/mixins/courseware/wizard.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-structural-element-dialog-add',
+    mixins: [colorMixin, wizardMixin],
+    components: {
+        CoursewareCompanionBox,
+        StudipWizardDialog,
+        StudipSelect,
+    },
+    props: {
+        structuralElement: Object,
+        isRoot: Boolean,
+        canEditParent: Boolean,
+    },
+    data() {
+        return {
+            wizardSlots: [
+                {
+                    id: 1,
+                    valid: false,
+                    name: 'basic',
+                    title: this.$gettext('Grundeinstellungen'),
+                    icon: 'courseware',
+                    description: this.$gettext(
+                        'Wählen Sie einen kurzen, prägnanten Titel und beschreiben Sie in einigen Worten den Inhalt der Seite.'
+                    ),
+                },
+                {
+                    id: 2,
+                    valid: true,
+                    name: 'template',
+                    title: this.$gettext('Vorlage'),
+                    icon: 'content2',
+                    description: this.$gettext('Vorlagen enthalten Abschnitte und Blöcke, die bereits für bestimmte Zwecke angeordent sind. Beim anlegen der Seite, wird diese mit Abschnitten und Blöcken befüllt.'),
+                },
+                {
+                    id: 3,
+                    valid: true,
+                    name: 'layout',
+                    title: this.$gettext('Erscheinung'),
+                    icon: 'picture',
+                    description: this.$gettext(
+                        'Ein Vorschaubild motiviert Lernende die Seite zu erkunden. Die Kombination aus Bild und Farbe erleichtert das wiederfinden der Seite in einem Inhaltsverzeichnisblock.'
+                    ),
+                },
+                {
+                    id: 4,
+                    valid: true,
+                    name: 'advanced',
+                    title: this.$gettext('Zusatzangaben'),
+                    icon: 'info-list',
+                    description: this.$gettext(
+                        'Hier können Sie detaillierte Angaben zur Seite eintragen. Diese sind besonders interessant wenn die Seite als OER geteilt wird.'
+                    ),
+                },
+            ],
+            text: {
+                title: this.$gettext('Titel der neuen Seite'),
+            },
+            uploadFileError: '',
+            requirements: [],
+
+            pageParent: '',
+            title: '',
+            description: '',
+            purpose: '',
+            color: '',
+            license_type: '',
+            required_time: '',
+            difficulty_start: '',
+            difficulty_end: '',
+            templatePurpose: '',
+            selectedTemplate: null,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            licenses: 'licenses',
+            context: 'context',
+            lastCreatedStructuralElement: 'courseware-structural-elements/lastCreated',
+            structuralElementById: 'courseware-structural-elements/byId',
+            templates: 'courseware-templates/all',
+        }),
+        inCourseContext() {
+            return this.context.type === 'courses';
+        },
+        colors() {
+            return this.mixinColors.filter((color) => color.darkmode);
+        },
+        selectableTemplates() {
+            return this.templates.filter((template) => {
+                return template.attributes.purpose === this.templatePurpose;
+            });
+        },
+        hasTemplates() {
+            return this.templates.length > 0;
+        },
+    },
+    methods: {
+        ...mapActions({
+            createStructuralElementWithTemplate: 'createStructuralElementWithTemplate',
+            updateStructuralElement: 'updateStructuralElement',
+            companionError: 'companionError',
+            companionInfo: 'companionInfo',
+            companionSuccess: 'companionSuccess',
+            showElementAddDialog: 'showElementAddDialog',
+            loadStructuralElementById: 'courseware-structural-elements/loadById',
+            uploadImageForStructuralElement: 'uploadImageForStructuralElement',
+        }),
+        initWizardData() {
+            this.pageParent = 'descendant';
+            this.title = '';
+            this.description = '';
+            this.purpose = 'content';
+            this.color = 'studip-blue';
+            this.license_type = '';
+            this.required_time = '';
+            this.difficulty_start = '';
+            this.difficulty_end = '';
+            this.templatePurpose = 'content';
+            this.selectedTemplate = null;
+            this.requirements.push({ slot: this.wizardSlots[0], text: this.text.title });
+        },
+        closeAddDialog() {
+            this.showElementAddDialog(false);
+            this.initWizardData();
+        },
+        checkUploadFile() {
+            this.uploadFileError = this.checkUploadImageFile(this.$refs?.upload_image?.files[0]);
+        },
+        async createElement() {
+            let parent_id = this.structuralElement.id; // new page is descandant as default
+
+            this.errorEmptyChapterName = this.title.trim();
+            if (this.errorEmptyChapterName === '') {
+                return;
+            }
+            if (this.pageParent === 'sibling') {
+                parent_id = this.structuralElement.relationships.parent.data.id;
+            }
+            this.showElementAddDialog(false);
+
+            const file = this.$refs?.upload_image?.files[0];
+            const element = {
+                attributes: {
+                    title: this.title,
+                    purpose: this.purpose,
+                    payload: {
+                        description: this.description,
+                        color: this.color,
+                        license_type: this.license_type,
+                        required_time: this.required_time,
+                        difficulty_start: this.difficulty_start,
+                        difficulty_end: this.difficulty_end,
+                    },
+                },
+                templateId: this.selectedTemplate ? this.selectedTemplate.id : null,
+                parentId: parent_id,
+                currentId: this.structuralElement.id,
+            };
+
+            try {
+                await this.createStructuralElementWithTemplate(element);
+            } catch (e) {
+                let errorMessage = this.$gettext(
+                    'Es ist ein Fehler aufgetreten. Die Seite konnte nicht erstellt werden.'
+                );
+                if (e.status === 403) {
+                    errorMessage = this.$gettext(
+                        'Die Seite konnte nicht erstellt werden. Sie haben nicht die notwendigen Schreibrechte.'
+                    );
+                }
+
+                this.companionError({ info: errorMessage });
+                return;
+            }
+
+            const newCreated = this.lastCreatedStructuralElement;
+            await this.loadStructuralElementById({ id: newCreated.id });
+            const newElement = this.structuralElementById({ id: newCreated.id });
+            this.companionSuccess({
+                info: this.$gettextInterpolate(this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'), {
+                    pageTitle: newElement.attributes.title,
+                }),
+            });
+
+            if (file && this.uploadFileError === '') {
+                try {
+                    await this.uploadImageForStructuralElement({ structuralElement: newElement, file });
+                } catch (error) {
+                    console.error(error);
+                    this.companionError({
+                        info: this.$gettext('Das Bild für das neue Lernmaterial konnte nicht gespeichert werden.'),
+                    });
+                }
+                this.loadStructuralElementById({ id: newElement.id, options: { include: 'children' } });
+            }
+            this.initWizardData();
+            this.$router.push(newElement.id);
+        },
+    },
+    mounted() {
+        this.initWizardData();
+        if (!this.hasTemplates) {
+                this.wizardSlots.splice(1,1);
+                this.wizardSlots[1]['id'] = 2;
+                this.wizardSlots[2]['id'] = 3;
+        }
+    },
+    watch: {
+        title(newTitle) {
+            this.requirements = [];
+            const slot = this.wizardSlots[0];
+            if (newTitle === '') {
+                slot.valid = false;
+                this.requirements.push({ slot: slot, text: this.text.title });
+            } else {
+                slot.valid = true;
+            }
+        },
+        templatePurpose(newPurpose) {
+            this.selectedTemplate = null;
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
new file mode 100644
index 0000000..82539ec
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
@@ -0,0 +1,477 @@
+<template>
+    <studip-wizard-dialog
+        :title="$gettext('Seiten kopieren')"
+        :confirmText="$gettext('Kopieren')"
+        :closeText="$gettext('Abbrechen')"
+        :lastRequiredSlotId="3"
+        :requirements="requirements"
+        :slots="wizardSlots"
+        @close="showElementCopyDialog(false)"
+        @confirm="copyElement"
+    >
+        <template v-slot:source>
+            <form class="default" @submit.prevent="">
+                <fieldset class="radiobutton-set">
+                    <input
+                        id="cw-element-copy-source-self"
+                        v-if="inCourseContext"
+                        type="radio"
+                        v-model="source"
+                        value="self"
+                        :aria-description="text.sourceSelf"
+                    />
+                    <label v-if="inCourseContext" @click="source = 'self'" for="cw-element-copy-source-self">
+                        <div class="icon"><studip-icon shape="seminar" :size="32"/></div>
+                        <div class="text">{{ text.sourceSelf }}</div>
+                        <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
+                        <studip-icon shape="check-circle" :size="24" class="check" />
+                    </label>
+                    <input
+                        id="cw-element-copy-source-courses"
+                        type="radio"
+                        v-model="source"
+                        value="courses"
+                        :aria-description="text.sourceCourses"
+                    />
+                    <label @click="source = 'courses'" for="cw-element-copy-source-courses">
+                        <div class="icon"><studip-icon shape="seminar" :size="32"/></div>
+                        <div class="text">{{ text.sourceCourses }}</div>
+                        <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
+                        <studip-icon shape="check-circle" :size="24" class="check" />
+                    </label>
+                    <input
+                        id="cw-element-copy-source-users"
+                        type="radio"
+                        v-model="source"
+                        value="users"
+                        :aria-description="text.sourceUsers"
+                    />
+                    <label @click="source = 'users'" for="cw-element-copy-source-users">
+                        <div class="icon"><studip-icon shape="content" :size="32"/></div>
+                        <div class="text">{{ text.sourceUsers }}</div>
+                        <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
+                        <studip-icon shape="check-circle" :size="24" class="check" />
+                    </label>
+                </fieldset>
+                <template v-if="source === 'courses'">
+                    <label>
+                        <span>{{ $gettext('Semester') }}</span><span aria-hidden="true"></span>
+                        <select v-model="selectedSemester">
+                            <option value="all">{{ $gettext('Alle Semester') }}</option>
+                            <option v-for="semester in semesterMap" :key="semester.id" :value="semester.id">
+                                {{ semester.attributes.title }}
+                            </option>
+                        </select>
+                    </label>
+                    <label>
+                        <span>{{ $gettext('Veranstaltung') }}</span><span aria-hidden="true" class="wizard-required">*</span>
+                        <studip-select
+                            v-if="filteredCourses.length !== 0 && !loadingCourses"
+                            :options="filteredCourses"
+                            :clearable="false"
+                            :reduce="option => option.id"
+                            :getOptionLabel="option => option.attributes.title"
+                            v-model="selectedRange"
+                        >
+                            <template #open-indicator="selectAttributes">
+                                <span v-bind="selectAttributes"
+                                    ><studip-icon shape="arr_1down" :size="10"
+                                /></span>
+                            </template>
+                        </studip-select>
+                        <p v-if="loadingCourses">
+                            {{$gettext('Lade Veranstaltungen…')}}
+                        </p>
+                        <p v-if="filteredCourses.length === 0 && !loadingCourses">
+                            {{$gettext('Es wurden keine geeigneten Veranstaltungen gefunden.')}}
+                        </p>
+                    </label>
+                </template>
+
+            </form>
+        </template>
+        <template v-slot:unit>
+            <form class="default" @submit.prevent="">
+                <fieldset v-if="units.length !== 0" class="radiobutton-set">
+                    <template v-for="unit in units">
+                        <input
+                            :id="'cw-element-copy-unit-' + unit.id"
+                            type="radio"
+                            v-model="selectedUnit"
+                            :checked="unit.id === selectedUnitId"
+                            :value="unit"
+                            :key="'radio-' + unit.id"
+                            :aria-description="unit.element.attributes.title"
+                        />
+                        <label :key="'label-' + unit.id" :for="'cw-element-copy-unit-' + unit.id">
+                            <div class="icon"><studip-icon shape="courseware" :size="32"/></div>
+                            <div class="text">{{ unit.element.attributes.title }}</div>
+                            <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
+                            <studip-icon shape="check-circle" :size="24" class="check" />
+                        </label>
+                    </template>
+                </fieldset>
+                <courseware-companion-box
+                    v-else
+                    mood="sad"
+                    :msgCompanion="$gettext('Für die gewählte Quelle stehen kein Lernmaterialien zur Verfügung.')"
+                />
+            </form>
+        </template>
+        <template v-slot:element>
+            <form v-if="selectedUnit" class="default" @submit.prevent="">
+                <courseware-structural-element-selector
+                    v-model="selectedElement"
+                    :rootId="selectedUnitRootId"
+                    :validateAncestors="true"
+                    :targetId="currentElement"
+                />
+            </form>
+            <courseware-companion-box
+                v-else
+                mood="pointing"
+                :msgCompanion="$gettext('Bitte wählen Sie ein Lernmaterial aus.')"
+            />
+        </template>
+        <template v-slot:edit>
+            <form v-if="selectedUnit" class="default" @submit.prevent="">
+                <label>
+                    {{$gettext('Titel')}}
+                    <input type="text" v-model="modifiedTitle" required />
+                </label>
+                <label>
+                    {{$gettext('Farbe')}}
+                    <studip-select
+                        v-model="modifiedColor"
+                        :options="colors"
+                        :reduce="(color) => color.class"
+                        :clearable="false"
+                        label="class"
+                    >
+                        <template #open-indicator="selectAttributes">
+                            <span v-bind="selectAttributes"
+                                ><studip-icon shape="arr_1down" :size="10"
+                            /></span>
+                        </template>
+                        <template #no-options>
+                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                        </template>
+                        <template #selected-option="{ name, hex }">
+                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                            ><span>{{ name }}</span>
+                        </template>
+                        <template #option="{ name, hex }">
+                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                            ><span>{{ name }}</span>
+                        </template>
+                    </studip-select>
+                </label>
+                <label>
+                    {{$gettext('Beschreibung')}}
+                    <textarea v-model="modifiedDescription" required />
+                </label>
+            </form>
+            <courseware-companion-box
+                    v-else
+                    mood="pointing"
+                    :msgCompanion="$gettext('Bitte wählen Sie eine Seite aus.')"
+            />
+        </template>
+    </studip-wizard-dialog>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareStructuralElementSelector from './CoursewareStructuralElementSelector.vue';
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import StudipSelect from '../../StudipSelect.vue';
+import StudipWizardDialog from '../../StudipWizardDialog.vue';
+
+import { mapActions, mapGetters } from 'vuex'
+
+export default {
+    name: 'courseware-structural-element-dialog-copy',
+    mixins: [colorMixin],
+    components: {
+        CoursewareCompanionBox,
+        CoursewareStructuralElementSelector,
+        StudipWizardDialog,
+        StudipSelect,
+    },
+    data() {
+        return {
+            wizardSlots: [
+                { id: 1, valid: false, name: 'source', title: this.$gettext('Quelle'), icon: 'source',
+                  description: this.$gettext('Wählen Sie hier den Ort in Stud.IP aus, an dem sich der zu kopierende Lerninhalt befindet.') },
+                { id: 2, valid: false, name: 'unit', title: this.$gettext('Lernmaterial'), icon: 'courseware',
+                  description: this.$gettext('Wählen Sie das Lernmaterial aus, in dem sich der zu kopierende Lerninhalt befindet.') },
+                { id: 3, valid: false, name: 'element', title: this.$gettext('Seite'), icon: 'content2',
+                  description: this.$gettext('Wählen Sie die zu kopierende Seite aus. Um Unterseiten anzuzeigen, klicken Sie auf den Seitennamen. Mit einem weiteren Klick werden die Unterseiten wieder zugeklappt.') },
+                { id: 4, valid: true, name: 'edit', title: this.$gettext('Anpassen'), icon: 'edit',
+                  description: this.$gettext('Sie können hier die Daten der zu kopierenden Seite anpassen. Eine Anpassung ist optional, Sie können die Seite auch unverändert kopieren.') },
+            ],
+            source: '',
+            loadingCourses: false,
+            courses: [],
+            semesterMap: [],
+            selectedSemester: 'all',
+            selectedRange: '',
+            loadingUnits: false,
+            selectedUnit: null,
+            selectedElement: null,
+            modifiedTitle: '',
+            modifiedColor: '',
+            modifiedDescription: '',
+            requirements: [],
+            text: {
+                sourceSelf: this.$gettext('Diese Veranstaltung'),
+                sourceCourses: this.$gettext('Veranstaltung'),
+                sourceUsers: this.$gettext('Arbeitsplatz'),
+                source: this.$gettext('Quelle'),
+                unit: this.$gettext('Lernmaterial'),
+                element: this.$gettext('Seite'),
+            },
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            coursewareUnits: 'courseware-units/all',
+            semesterById: 'semesters/byId',
+            structuralElementById: 'courseware-structural-elements/byId',
+            context: 'context',
+            childrenById: 'courseware-structure/children',
+            currentElement: 'currentElement'
+        }),
+        colors() {
+            return this.mixinColors.filter(color => color.darkmode);
+        },
+        inCourseContext() {
+            return this.context.type === 'courses';
+        },
+        units() {
+            let units = this.coursewareUnits.filter(unit => unit.relationships.range.data.id === this.selectedRange);
+            units.forEach(unit => {
+                unit.element = this.getUnitElement(unit);
+            });
+
+            return units;
+        },
+        selectedUnitId() {
+            return this.selectedUnit?.id;
+        }, 
+        selectedUnitRootId() {
+            return this.selectedUnit?.relationships?.['structural-element']?.data?.id;
+        }, 
+        selectedElementTitle() {
+            return this.selectedElement?.attributes?.title;
+        },
+        selectedElementParent() {
+            let parentData = this.selectedElement?.relationships?.parent?.data;
+            if (parentData){
+                return this.structuralElementById({id: parentData.id});
+            }
+
+            return null;
+        },
+        selectedElementParentTitle() {
+            if (this.selectedElementParent) {
+                return this.selectedElementParent.attributes.title;
+            }
+
+            return '';
+        },
+        children() {
+            if (!this.selectedElement) {
+                return [];
+            }
+
+            return this.childrenById(this.selectedElement.id)
+                .map((id) => this.structuralElementById({ id }))
+                .filter(Boolean);
+        },
+        filteredCourses() {
+            const courses = this.courses.filter((course) => { return course.id !== this.context.id });
+            if (this.selectedSemester === 'all') {
+                return courses;
+            } else {
+                return courses.filter((course) => {
+                    return course.relationships['start-semester'].data.id === this.selectedSemester;
+                });
+            }
+        }
+    },
+    mounted() {
+        this.initWizardData();
+    },
+    methods: {
+        ...mapActions({
+            showElementCopyDialog: 'showElementCopyDialog',
+            loadCourseUnits: 'loadCourseUnits',
+            loadUserUnits: 'loadUserUnits',
+            loadUsersCourses: 'loadUsersCourses',
+            loadSemester: 'semesters/loadById',
+            loadStructuralElement: 'courseware-structural-elements/loadById',
+            copyStructuralElement: 'copyStructuralElement',
+            companionError: 'companionError',
+            companionSuccess: 'companionSuccess',
+        }),
+        initWizardData() {
+            this.source = this.inCourseContext ? 'self' : 'users';
+            this.selectedRange = '';
+            this.selectedUnit = null;
+        },
+        getUnitElement(unit) {
+            return this.structuralElementById({id: unit.relationships['structural-element'].data.id});
+        },
+        async updateCourses() {
+            this.loadingCourses = true;
+            this.courses = await this.loadUsersCourses({ userId: this.userId, withCourseware: true });
+            this.loadSemesterMap();
+            this.loadingCourses = false;
+        },
+        loadSemesterMap() {
+            let view = this;
+            let semesters = [];
+            this.courses.every(course => {
+                let semId = course.relationships['start-semester'].data.id;
+                if(!semesters.includes(semId)) {
+                    semesters.push(semId);
+                }
+                return true;
+            });
+            semesters.every(semester => {
+                view.loadSemester({id: semester}).then( () => {
+                    view.semesterMap.push(view.semesterById({id: semester}));
+                    view.semesterMap.sort((a, b) => new Date(b.attributes.start) - new Date(a.attributes.start));
+                });
+                return true;
+            });
+        },
+        async updateCourseUnits(cid) {
+            this.loadingUnits = true;
+            await this.loadCourseUnits(cid);
+            this.loadingUnits = false;
+        },
+        async updateUserUnits() {
+            this.loadingUnits = true;
+            await this.loadUserUnits(this.userId);
+            this.loadingUnits = false;
+        },
+        selectElement(id) {
+            this.selectedElement = this.structuralElementById({id: id});
+            this.loadStructuralElement({id: id, options: {include: 'children'}});
+        },
+        setElementData() {
+            this.modifiedTitle = this.selectedElement.attributes.title;
+            this.modifiedColor = this.selectedElement.attributes.payload.color;
+            this.modifiedDescription = this.selectedElement.attributes.payload.description;
+        },
+        resetElementData() {
+            this.modifiedTitle = '';
+            this.modifiedColor = '';
+            this.modifiedDescription = '';
+        },
+        copyElement() {
+            let view = this;
+            this.copyStructuralElement({
+                    parentId: this.currentElement,
+                    elementId: this.selectedElement.id,
+                    migrate: false,
+                    modifications: {
+                        title: view.modifiedTitle,
+                        color: view.modifiedColor,
+                        description: view.modifiedDescription
+                    }
+            })
+            .then( () => {
+                view.companionSuccess({
+                    info: view.$gettextInterpolate(
+                        view.$gettext('Die Seite %{ pageTitle } wurde erfolgreich kopiert.'),
+                        {pageTitle: view.selectedElementTitle}
+                    )
+                });
+            })
+            .catch(error => {
+                view.companionError({
+                    info: view.$gettextInterpolate(
+                        view.$gettext('Die Seite %{ pageTitle } konnte nicht kopiert werden.'),
+                        {pageTitle: view.selectedElementTitle}
+                    )
+                });
+            })
+            .finally(() => {
+                view.showElementCopyDialog(false);
+            });
+        },
+        validateSelection() {
+            this.requirements = [];
+            if (this.selectedRange === '') {
+                this.requirements.push({slot: this.wizardSlots[0], text: this.text.source });
+            }
+            if (this.selectedUnit === null) {
+                this.requirements.push({slot: this.wizardSlots[1], text: this.text.unit });
+            }
+            if (this.selectedUnit === null) {
+                this.requirements.push({slot: this.wizardSlots[2], text: this.text.element });
+            }
+        }
+    },
+    watch: {
+        selectedElement(newElement) {
+            this.validateSelection();
+            if (newElement !== null) {
+                this.wizardSlots[2].valid = true;
+                this.setElementData();
+            } else {
+                this.resetElementData();
+                this.wizardSlots[2].valid = false;
+            }
+            
+        },
+        async selectedUnit(newUnit) {
+            this.validateSelection();
+            if (newUnit !== null) {
+                this.wizardSlots[1].valid = true;
+                await this.loadStructuralElement({id: this.selectedUnitRootId, options: {include: 'children'}});
+                this.selectedElement = null;
+            } else {
+                this.wizardSlots[1].valid = false;
+            }
+            
+        },
+        selectedRange(newRid) {
+            this.validateSelection();
+            this.selectedUnit = null;
+            if (newRid !== '') {
+                this.wizardSlots[0].valid = true;
+                if (this.source === 'courses' || this.source === 'self') {
+                    this.updateCourseUnits(newRid);
+                }
+                if (this.source === 'users') {
+                    this.loadUserUnits(newRid);
+                }
+            } else {
+                this.wizardSlots[0].valid = false;
+            }
+        },
+        source(newSource) {
+            switch (newSource) {
+                case 'self':
+                    this.selectedRange = this.context.id;
+                    break;
+                case 'courses':
+                    this.selectedRange = '';
+                    this.updateCourses();
+                    break;
+                case 'users':
+                    this.selectedRange = this.userId;
+                    break;
+            }
+        },
+        selectedSemester(newSemester) {
+            this.selectedRange = '';
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue
new file mode 100644
index 0000000..ba940a6
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue
@@ -0,0 +1,200 @@
+<template>
+    <studip-dialog
+        :title="$gettext('Seiten importieren')"
+        :confirmText="$gettext('Importieren')"
+        :confirmDisabled="importRunning || importAborted"
+        :closeText="importRunning || importAborted ? $gettext('Schließen') : $gettext('Abbrechen')"
+        height="420"
+        @close="showElementImportDialog(false)"
+        @confirm="importCoursewareArchiv"
+    >
+        <template v-slot:dialogContent>
+            <form v-if="!importRunning && !importAborted" class="default" @submit.prevent="">
+                <label>
+                    {{$gettext('Importdatei')}}
+                    <input class="cw-file-input" ref="importFile" type="file" accept=".zip" @change="setImport" />
+                </label>
+                <label>
+                {{$gettext('Importverhalten')}}
+                <select v-model="importBehavior">
+                    <option value="default">{{$gettext('Inhalte anhängen')}}</option>
+                    <option value="migrate">{{$gettext('Inhalte zusammenführen')}}</option>
+                </select>
+            </label>
+            </form>
+            <div role="status" aria-live="polite">
+                <courseware-companion-box 
+                    v-show="importDone && importErrors.length === 0"
+                    :msgCompanion="$gettext('Import erfolgreich!')"
+                    mood="special"
+                />
+                <courseware-companion-box
+                    v-show="importDone && importErrors.length > 0"
+                    :msgCompanion="$gettext('Import abgeschlossen. Es sind Fehler aufgetreten!')"
+                    mood="unsure"
+                />
+                <courseware-companion-box
+                    v-show="!importDone && importRunning"
+                    :msgCompanion="$gettext('Import läuft. Bitte schließen Sie den Dialog nicht bis der Import abgeschlossen wurde.')"
+                    mood="pointing"
+                />
+                <courseware-companion-box
+                    v-show="importAborted"
+                    :msgCompanion="$gettext('Import abgebrochen. Es sind Fehler aufgetreten!')"
+                    mood="sad"
+                />
+            </div>
+            <form v-if="!importDone && importRunning" class="default" @submit.prevent="">
+                <fieldset>
+                    <div v-if="!fileImportDone" class="cw-import-zip">
+                        <header>{{$gettext('Importiere Dateien')}}:</header>
+                        <div class="progress-bar-wrapper">
+                            <div class="progress-bar" role="progressbar" :style="{width: importFilesProgress + '%'}" :aria-valuenow="importFilesProgress" aria-valuemin="0" aria-valuemax="100">{{ importFilesProgress }}%</div>
+                        </div>
+                        {{ importFilesState }}
+                    </div>
+                    <div v-if="fileImportDone" class="cw-import-zip">
+                        <header>{{$gettext('Importiere Elemente')}}:</header>
+                        <div class="progress-bar-wrapper">
+                            <div class="progress-bar" role="progressbar" :style="{width: importStructuresProgress + '%'}" :aria-valuenow="importStructuresProgress" aria-valuemin="0" aria-valuemax="100">{{ importStructuresProgress }}%</div>
+                        </div>
+                        {{ importStructuresState }}
+                    </div>
+                </fieldset>
+            </form>
+            <form v-if="importErrors.length > 0" class="default" @submit.prevent="">
+                <fieldset>
+                    <legend>{{$gettext('Fehlermeldungen')}}</legend>
+                    <ul>
+                        <li v-for="(error, index) in importErrors" :key="index"> {{error}} </li>
+                    </ul>
+                </fieldset>
+            </form>
+        </template>
+    </studip-dialog>
+</template>
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareImport from '@/vue/mixins/courseware/import.js';
+
+import { mapActions, mapGetters } from 'vuex'
+import JSZip from 'jszip';
+
+export default {
+    name: 'courseware-structural-element-dialog-import',
+    components: {
+        CoursewareCompanionBox,
+    },
+    mixins: [CoursewareImport],
+    props: {
+        colors: Array
+    },
+    data() {
+        return {
+            importBehavior: 'default',
+            importRunning: false,
+            importZipFile: null,
+            zip: null,
+            importAborted: false,
+        }
+    },
+    computed: {
+        ...mapGetters({
+            currentElement: 'currentElement',
+            importFilesState: 'importFilesState',
+            importFilesProgress: 'importFilesProgress',
+            importStructuresState: 'importStructuresState',
+            importStructuresProgress: 'importStructuresProgress',
+            importErrors: 'importErrors',
+        }),
+        fileImportDone() {
+            return this.importFilesProgress === 100;
+        },
+        importDone() {
+            return (this.importFilesProgress === 100 && this.importStructuresProgress === 100);
+        }
+    },
+    methods: {
+        ...mapActions({
+            showElementImportDialog: 'showElementImportDialog',
+            loadCoursewareStructure: 'courseware-structure/load',
+            setImportFilesProgress: 'setImportFilesProgress',
+            setImportStructuresProgress: 'setImportStructuresProgress',
+            setImportErrors: 'setImportErrors',
+        }),
+        setImport(event) {
+            this.importZipFile = event.target.files[0];
+            this.setImportFilesProgress(0);
+            this.setImportStructuresProgress(0);
+            this.setImportErrors([]);
+        },
+        async importCoursewareArchiv() {
+            this.importAborted = false;
+            if (this.importZipFile === null) {
+                return false;
+            }
+
+            this.importRunning = true;
+            try {
+                this.zip = await JSZip.loadAsync(this.importZipFile);
+            } catch(error) {
+                this.setImportErrors([this.$gettext('Die gewählte Datei ist kein Archiv oder das Archiv ist beschädigt.')]);
+                this.importRunning = false;
+                this.importAborted = true;
+                return;
+            }
+            let errors = [];
+            let missingFiles = false;
+            if (this.zip.file('courseware.json') === null) {
+                errors.push(this.$gettext('Das Archiv enthält keine courseware.json Datei.'));
+                missingFiles = true;
+            }
+            if (this.zip.file('files.json') === null) {
+                errors.push(this.$gettext('Das Archiv enthält keine files.json Datei.'));
+                missingFiles = true;
+            }
+            if (this.zip.file('data.xml') !== null) {
+                errors.push(this.$gettext(
+                    'Das Archiv enthält eine data.xml Datei. Möglicherweise handelt es sich um einen Export aus dem Courseware-Plugin. Diese Archive sind nicht kompatibel mit dieser Courseware.'
+                ));
+            }
+            if (missingFiles) {
+                this.setImportErrors(errors);
+                this.importRunning = false;
+                this.importAborted = true;
+                return;
+            }
+
+            const data = await this.zip.file('courseware.json').async('string');
+            let courseware = null;
+            const data_files = await this.zip.file('files.json').async('string');
+            let files = null;
+            let jsonErrors = false;
+            try {
+                courseware = JSON.parse(data);
+            } catch (error) {
+                jsonErrors = true;
+                errors.push(this.$gettext('Die Beschreibung der Courseware-Inhalte ist nicht valide.'));
+                errors.push(error);
+            }
+            try {
+                files = JSON.parse(data_files);
+            } catch (error) {
+                jsonErrors = true;
+                errors.push(this.$gettext('Die Beschreibung der Dateien ist nicht valide.'));
+                errors.push(error);
+            }
+            if (jsonErrors) {
+                this.setImportErrors(errors);
+                this.importRunning = false;
+                this.importAborted = true;
+                return;
+            }
+
+            await this.loadCoursewareStructure();
+
+            await this.importCourseware(courseware, this.currentElement, files, this.importBehavior, null);
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogLink.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogLink.vue
new file mode 100644
index 0000000..1405e39
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogLink.vue
@@ -0,0 +1,232 @@
+<template>
+    <studip-wizard-dialog
+        :title="$gettext('Seiten verknüpfen')"
+        :confirmText="$gettext('Verknüpfen')"
+        :closeText="$gettext('Abbrechen')"
+        :lastRequiredSlotId="2"
+        :requirements="requirements"
+        :slots="wizardSlots"
+        @close="showElementLinkDialog(false)"
+        @confirm="linkElement"
+    >
+        <template v-slot:unit>
+            <form v-if="!loadingUnits" class="default" @submit.prevent="">
+                <fieldset v-if="hasUnits" class="radiobutton-set">
+                    <template v-for="unit in units">
+                        <input
+                            :id="'cw-element-link-unit-' + unit.id"
+                            type="radio"
+                            :checked="unit.id === selectedUnitId"
+                            :value="unit.id"
+                            :key="'radio-' + unit.id"
+                            :aria-description="unit.element.attributes.title"
+                        />
+                        <label @click="selectedUnit = unit" :key="'label-' + unit.id" :for="'cw-element-link-unit-' + unit.id">
+                            <div class="icon"><studip-icon shape="courseware" size="32"/></div>
+                            <div class="text">{{ unit.element.attributes.title }}</div>
+                            <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
+                            <studip-icon shape="check-circle" size="24" class="check" />
+                        </label>
+                    </template>
+                </fieldset>
+                <courseware-companion-box
+                    v-else
+                    mood="sad"
+                    :msgCompanion="$gettext('Es konnte leider kein Lernmaterial gefunden werden. Bitte erstellen Sie unter Arbeitsplatz/Courseware ein Lernmaterial.')"
+                />
+            </form>
+            <studip-progress-indicator 
+                v-else
+                :description="$gettext('Lade Lernmaterialien…')"
+            />
+        </template>
+        <template v-slot:element>
+            <form v-if="selectedUnit" class="default" @submit.prevent="">
+                <courseware-structural-element-selector
+                    v-model="selectedElement"
+                    :rootId="selectedUnitRootId"
+                />
+            </form>
+            <courseware-companion-box
+                v-else
+                mood="pointing"
+                :msgCompanion="$gettext('Bitte wählen Sie zuerst das Lernmaterial aus.')"
+            />
+        </template>
+    </studip-wizard-dialog>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareStructuralElementSelector from './CoursewareStructuralElementSelector.vue';
+import StudipWizardDialog from '../../StudipWizardDialog.vue';
+import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
+
+import { mapActions, mapGetters } from 'vuex'
+
+export default {
+    name: 'courseware-structural-element-dialog-link',
+    components: {
+        CoursewareCompanionBox,
+        CoursewareStructuralElementSelector,
+        StudipWizardDialog,
+        StudipProgressIndicator
+    },
+    data() {
+        return {
+            wizardSlots: [
+                {id: 1, valid: false, name: 'unit', title: this.$gettext('Lernmaterial'), icon: 'courseware', 
+                description: this.$gettext('Wählen Sie das Lernmaterial aus, in dem sich der zu verknüpfende Lerninhalt befindet. Die Lerninhalte, die verknüpft werden können, müssen unter Arbeitsplatz/Courseware vorher erstellt werden.')},
+                {id: 2, valid: false, name: 'element', title: this.$gettext('Seite'), icon: 'content2', 
+                description: this.$gettext('Wählen Sie die zu verknüpfende Seite aus. Um Unterseiten anzuzeigen, klicken Sie auf den Seitennamen. Mit einem weiteren Klick werden die Unterseiten wieder zugeklappt.')},            ],
+            loadingUnits: false,
+            selectedUnit: null,
+            selectedElement: null,
+            requirements: [],
+            text: {
+
+            }
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            coursewareUnits: 'courseware-units/all',
+            structuralElementById: 'courseware-structural-elements/byId',
+            context: 'context',
+            childrenById: 'courseware-structure/children',
+            currentElement: 'currentElement'
+        }),
+        units() {
+            let units = this.coursewareUnits.filter(unit => unit.relationships.range.data.id === this.userId);
+            units.forEach(unit => {
+                unit.element = this.getUnitElement(unit);
+            });
+
+            return units;
+        },
+        hasUnits() {
+            return this.units.length !== 0;
+        },
+        selectedUnitId() {
+            return this.selectedUnit?.id;
+        }, 
+        selectedUnitRootId() {
+            return this.selectedUnit?.relationships?.['structural-element']?.data?.id;
+        }, 
+        selectedElementTitle() {
+            return this.selectedElement?.attributes?.title;
+        },
+        selectedElementParent() {
+            let parentData = this.selectedElement?.relationships?.parent?.data;
+            if (parentData){
+                return this.structuralElementById({id: parentData.id});
+            }
+
+            return null;
+        },
+        selectedElementParentTitle() {
+            if (this.selectedElementParent) {
+                return this.selectedElementParent.attributes.title;
+            }
+
+            return '';
+        },
+        children() {
+            if (!this.selectedElement) {
+                return [];
+            }
+
+            return this.childrenById(this.selectedElement.id)
+                .map((id) => this.structuralElementById({ id }))
+                .filter(Boolean);
+        },
+    },
+    mounted() {
+        this.initWizardData();
+        this.updateUserUnits();
+        },
+    methods: {
+        ...mapActions({
+            showElementLinkDialog: 'showElementLinkDialog',
+            loadUserUnits: 'loadUserUnits',
+            loadStructuralElement: 'courseware-structural-elements/loadById',
+            linkStructuralElement: 'linkStructuralElement',
+            companionError: 'companionError',
+            companionSuccess: 'companionSuccess',
+        }),
+        initWizardData() {
+            this.selectedRange = '';
+            this.selectedUnit = null;
+            this.validateSelection();
+        },
+        async updateUserUnits() {
+            this.loadingUnits = true;
+            await this.loadUserUnits(this.userId);
+            this.loadingUnits = false;
+        },
+        getUnitElement(unit) {
+            return this.structuralElementById({id: unit.relationships['structural-element'].data.id});
+        },
+        linkElement() {
+            let view = this;
+            this.linkStructuralElement({
+                parentId: this.currentElement,
+                        elementId: this.selectedElement.id,
+                })
+                .then( () => {
+                    view.companionSuccess({
+                        info: view.$gettextInterpolate(
+                            view.$gettext('Die Seite %{ pageTitle } wurde erfolgreich verknüpft.'),
+                            { pageTitle: view.selectedElementTitle }
+                        )
+                    });
+                })
+                .catch( () => {
+                    view.companionError({
+                        info: view.$gettextInterpolate(
+                            view.$gettext('Die Seite %{ pageTitle } konnte nicht verknüpft werden.'),
+                            { pageTitle: view.selectedElementTitle }
+                        )
+                    });
+                })
+                .finally(() => {
+                    view.showElementLinkDialog(false);
+                });
+        },
+        selectElement(id) {
+            this.selectedElement = this.structuralElementById({id: id});
+            this.loadStructuralElement({id: id, options: {include: 'children'}});
+        },
+        validateSelection() {
+            this.requirements = [];
+            if (this.selectedUnit === null) {
+                this.requirements.push({slot: this.wizardSlots[0], text: this.$gettext('Lernmaterial') });
+            }
+            if (this.selectedElement === null) {
+                this.requirements.push({slot: this.wizardSlots[1], text: this.$gettext('Seite') });
+            }
+        }
+    },
+    watch: {
+        selectedElement(newElement) {
+            this.validateSelection();
+            if (newElement !== null) {
+                this.wizardSlots[1].valid = true;
+            } else {
+                this.wizardSlots[1].valid = false;
+            }
+        },
+        async selectedUnit(newUnit) {
+            this.validateSelection();
+            if (newUnit !== null) {
+                this.wizardSlots[0].valid = true;
+                await this.loadStructuralElement({id: this.selectedUnitRootId, options: {include: 'children'}});
+                this.selectedElement = null;
+            } else {
+                this.wizardSlots[0].valid = false;
+            }
+        },
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDiscussion.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDiscussion.vue
new file mode 100644
index 0000000..3428d83
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDiscussion.vue
@@ -0,0 +1,60 @@
+<template>
+    <div class="cw-structural-element-discussion">
+        <courseware-collapsible-box
+            :title="text.comments"
+            :open="hasComments"
+        >
+            <courseware-structural-element-comments
+            :structuralElement="structuralElement"
+            @hasComments="hasComments = true"
+            />
+        </courseware-collapsible-box>
+
+        <courseware-collapsible-box
+            v-if="canEdit || userIsTeacher"
+            :title="text.feedback"
+            :open="hasFeedback"
+        >
+            <courseware-structural-element-feedback
+                :structuralElement="structuralElement"
+                :canEdit="canEdit"
+                @hasFeedback="hasFeedback = true"
+            />
+        </courseware-collapsible-box>
+    </div>
+</template>
+
+<script>
+import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue';
+import CoursewareStructuralElementComments from './CoursewareStructuralElementComments.vue';
+import CoursewareStructuralElementFeedback from './CoursewareStructuralElementFeedback.vue';
+import { mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-structural-element-discussion',
+    components: {
+        CoursewareCollapsibleBox,
+        CoursewareStructuralElementComments,
+        CoursewareStructuralElementFeedback,
+    },
+    props: {
+        structuralElement: Object,
+        canEdit: Boolean
+    },
+    data() {
+        return {
+            hasComments: false,
+            hasFeedback: false,
+            text: {
+                comments: this.$gettext('Kommentare zur Seite'),
+                feedback: this.$gettext('Feedback zur Seite')
+            }
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userIsTeacher: 'userIsTeacher',
+        }),
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue
new file mode 100644
index 0000000..fd950bc
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue
@@ -0,0 +1,141 @@
+<template>
+    <section
+        v-if="canEdit || userIsTeacher"
+        class="cw-structural-element-feedback"
+        :class="[emptyFeedback ? 'cw-structural-element-feedback-empty' : '']"
+    >
+        <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
+        <div class="cw-structural-element-feedback-items" v-show="!emptyFeedback" ref="feedbacks">
+            <courseware-talk-bubble
+                v-for="feedback in feedback"
+                :key="feedback.id"
+                :payload="buildPayload(feedback)"
+            />
+        </div>
+        <courseware-companion-box
+                v-if="!userIsTeacher && feedback.length === 0"
+                :msgCompanion="$gettext('Es wurde noch kein Feedback abgegeben.')"
+                mood="pointing"
+            />
+        <div v-if="userIsTeacher" class="cw-structural-element-feedback-create">
+            <textarea v-model="feedbackText" :placeholder="placeHolder" spellcheck="true"></textarea>
+            <button class="button" @click="postFeedback"><translate>Senden</translate></button>
+        </div>
+    </section>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareTalkBubble from '../layouts/CoursewareTalkBubble.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-structural-element-feedback',
+    components: {
+        CoursewareCompanionBox,
+        CoursewareTalkBubble,
+    },
+    props: {
+        structuralElement: Object,
+        canEdit: Boolean,
+    },
+    data() {
+        return {
+            feedbackText: '',
+            placeHolder: this.$gettext('Schreiben Sie ein Feedback...'),
+            srMessage: ''
+        };
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            getRelatedFeedback: 'courseware-structural-element-feedback/related',
+            getRelatedUser: 'users/related',
+            userIsTeacher: 'userIsTeacher',
+        }),
+        feedback() {
+            const parent = {
+                type: this.structuralElement.type,
+                id: this.structuralElement.id,
+            };
+
+            return this.getRelatedFeedback({ parent, relationship: 'feedback' });
+        },
+        emptyFeedback() {
+            if (this.feedback === null || this.feedback.length === 0) {
+                return true;
+            }
+
+            return false;
+        }
+    },
+    methods: {
+        ...mapActions({
+            createFeedback: 'courseware-structural-element-feedback/create',
+            loadRelatedFeedback: 'courseware-structural-element-feedback/loadRelated',
+        }),
+        buildPayload(feedback) {
+            const { id, type } = feedback;
+            const user = this.getRelatedUser({ parent: { id, type }, relationship: 'user' });
+
+            return {
+                own: user.id === this.userId,
+                content: feedback.attributes.feedback,
+                chdate: feedback.attributes.chdate,
+                mkdate: feedback.attributes.mkdate,
+                user_name: user?.attributes?.['formatted-name'] ?? '',
+                user_avatar: user?.meta?.avatar.small,
+            };
+        },
+        async loadFeedback() {
+            const parent = {
+                type: this.structuralElement.type,
+                id: this.structuralElement.id,
+            };
+            await this.loadRelatedFeedback({
+                parent,
+                relationship: 'feedback',
+                options: {
+                    include: 'user',
+                },
+            });
+        },
+        async postFeedback() {
+            this.updateSrMessage(this.$gettext('Feedback gesendet'));
+            const data = {
+                attributes: {
+                    feedback: this.feedbackText,
+                },
+                relationships: {
+                    'structural-element': {
+                        data: {
+                            id: this.structuralElement.id,
+                            type: this.structuralElement.type
+                        }
+                    }
+                },
+            };
+            await this.createFeedback( data, { root: true });
+            this.feedbackText = '';
+            this.loadFeedback();
+        },
+        updateSrMessage(message) {
+            this.srMessage = '';
+            this.srMessage = message;
+        }
+    },
+    async mounted() {
+        await this.loadFeedback();
+    },
+    updated() {
+        this.$refs.feedbacks.scrollTop = this.$refs.feedbacks.scrollHeight;
+    },
+    watch: {
+        feedback() {
+            if (this.feedback && this.feedback.length > 0) {
+                this.$emit('hasFeedback');
+            }
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementPermissions.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementPermissions.vue
new file mode 100644
index 0000000..f09ed54
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementPermissions.vue
@@ -0,0 +1,471 @@
+<template>
+    <div class="cw-element-permissions">
+        <label>
+            <input type="checkbox" class="default" v-model="userPermsReadAll" />
+            <translate>Alle Teilnehmenden haben Leserechte</translate>
+        </label>
+        <label>
+            <input type="checkbox" class="default" v-model="userPermsWriteAll" />
+            <translate>Alle Teilnehmenden haben Schreibrechte</translate>
+        </label>
+
+        <table class="default" v-if="autor_members.length">
+            <caption>
+                <translate>Studierende</translate>
+            </caption>
+            <colgroup>
+                <col style="width:1%" />
+                <col style="width:19%" />
+                <col style="width:1%" />
+                <col style="width:29%" />
+                <col style="width:50%" />
+            </colgroup>
+            <thead>
+                <tr>
+                    <th><input type="checkbox" v-model="bulkSelectAutorRead" @click="handleBulkSelectRead($event, 'autor')"/></th>
+                    <th><translate>Lesen</translate></th>
+                    <th><input type="checkbox" v-model="bulkSelectAutorWrite" @click="handleBulkSelectWrite($event)"/></th>
+                    <th><translate>Lesen und Schreiben</translate></th>
+                    <th><translate>Name</translate></th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="user in autor_members_filtered" :key="user.user_id">
+                    <td class="perm" colspan="2">
+                        <input
+                            type="checkbox"
+                            :id="user.user_id + `_read`"
+                            :value="user.user_id"
+                            v-model="userPermsReadUsers"
+                        />
+                    </td>
+                    <td class="perm" colspan="2">
+                        <input
+                            type="checkbox"
+                            :id="user.user_id + `_write`"
+                            :value="user.user_id"
+                            v-model="userPermsWriteUsers"
+                        />
+                    </td>
+
+                    <td>
+                        <label :for="user.user_id + `_read`">
+                            {{ user.formattedname }}
+                            <i>{{ user.username }}</i>
+                        </label>
+                    </td>
+                </tr>
+            </tbody>
+            <tfoot v-if="can_paginate && autor_members.length > entries_per_page">
+                <tr>
+                    <td colspan="5">
+                        <studip-pagination
+                            :currentOffset="autorOffset"
+                            :totalItems="autor_members.length"
+                            :itemsPerPage="entries_per_page"
+                            @updateOffset="updateAutorOffset" />
+                    </td>
+                </tr>
+            </tfoot>
+        </table>
+
+        <table class="default" v-if="user_members.length">
+            <caption>
+                <translate>Leser/-innen</translate>
+            </caption>
+            <colgroup>
+                <col style="width:1%" />
+                <col style="width:39%" />
+                <col style="width:50%" />
+            </colgroup>
+            <thead>
+                <tr>
+                    <th><input type="checkbox" v-model="bulkSelectUserRead" @click="handleBulkSelectRead($event, 'user')"/></th>
+                    <th><translate>Lesen</translate></th>
+                    <th><translate>Name</translate></th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="user in user_members_filtered" :key="user.user_id">
+                    <td colspan="2">
+                        <input
+                            type="checkbox"
+                            :id="user.user_id + `_read`"
+                            :value="user.user_id"
+                            v-model="userPermsReadUsers"
+                        />
+                    </td>
+                    <td>
+                        <label :for="user.user_id + `_read`">
+                            {{ user.firstname }}
+                            {{ user.lastname }}
+                            <i>{{ user.username }}</i>
+                        </label>
+                    </td>
+                </tr>
+            </tbody>
+            <tfoot v-if="can_paginate && user_members.length > entries_per_page">
+                <tr>
+                    <td colspan="3">
+                        <studip-pagination
+                            :currentOffset="userOffset"
+                            :totalItems="user_members.length"
+                            :itemsPerPage="entries_per_page"
+                            @updateOffset="updateUserOffset"/>
+                    </td>
+                </tr>
+            </tfoot>
+        </table>
+
+        <table class="default" v-if="groups.length">
+            <caption>
+                <translate>Gruppen</translate>
+            </caption>
+            <colgroup>
+                <col style="width:20%" />
+                <col style="width:35%" />
+                <col style="width:45%" />
+            </colgroup>
+            <thead>
+                <tr>
+                    <th><translate>Lesen</translate></th>
+                    <th><translate>Lesen und Schreiben</translate></th>
+                    <th><translate>Name</translate></th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="group in groups" :key="group.id">
+                    <td class="perm">
+                        <input
+                            type="checkbox"
+                            :id="group.id + `_read`"
+                            :value="group.id"
+                            v-model="userPermsReadGroups"
+                        />
+                    </td>
+                    <td class="perm">
+                        <input
+                            type="checkbox"
+                            :id="group.id + `_write`"
+                            :value="group.id"
+                            v-model="userPermsWriteGroups"
+                        />
+                    </td>
+
+                    <td>
+                        <label :for="group.id + `_read`">
+                            {{ group.name }}
+                        </label>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</template>
+<script>
+import StudipPagination from '../../StudipPagination.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-structural-element-permissions',
+    props: {
+        element: Object,
+    },
+    components: {
+        StudipPagination
+    },
+    data() {
+        return {
+            user_perms: {},
+            userPermsReadUsers: [],
+            userPermsReadGroups: [],
+            userPermsReadAll: Boolean,
+            userPermsWriteUsers: [],
+            userPermsWriteGroups: [],
+            userPermsWriteAll: Boolean,
+            bulkSelectAutorRead: false,
+            bulkSelectUserRead: false,
+            bulkSelectAutorWrite: false,
+            userOffset: 0,
+            autorOffset: 0,
+        };
+    },
+
+    mounted() {
+        if (this.element.attributes['read-approval'].users !== undefined) {
+            this.userPermsReadUsers = this.element.attributes['read-approval'].users;
+        }
+        if (this.element.attributes['read-approval'].groups !== undefined) {
+            this.userPermsReadGroups = this.element.attributes['read-approval'].groups;
+        }
+        if (this.element.attributes['read-approval'].all !== undefined) {
+            this.userPermsReadAll = this.element.attributes['read-approval'].all;
+        } else {
+            this.userPermsReadAll = true;
+        }
+        if (this.element.attributes['write-approval'].users !== undefined) {
+            this.userPermsWriteUsers = this.element.attributes['write-approval'].users;
+        }
+        if (this.element.attributes['write-approval'].groups !== undefined) {
+            this.userPermsWriteGroups = this.element.attributes['write-approval'].groups;
+        }
+        if (this.element.attributes['write-approval'].all !== undefined) {
+            this.userPermsWriteAll = this.element.attributes['write-approval'].all;
+        } else {
+            this.userPermsWriteAll = false;
+        }
+
+        // load memberships for coursewares in a course context
+        if (this.context.type === 'courses') {
+            const parent = { type: 'courses', id: this.context.id };
+            let options = {
+                include: 'user',
+                'page[limit]': 10000,
+            }
+            this.loadCourseMemberships({ parent, relationship: 'memberships', options: options });
+            this.loadCourseStatusGroups({ parent, relationship: 'status-groups' });
+        }
+    },
+
+    updated () {
+        this.handleBulkSelectReadPassive('autor');
+        this.handleBulkSelectReadPassive('user');
+        this.handleBulkSelectWritePassive();
+    },
+
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            courseware: 'courseware',
+            course: 'courses/related',
+            relatedCourseMemberships: 'course-memberships/related',
+            relatedCourseStatusGroups: 'status-groups/related',
+            relatedUser: 'users/related',
+        }),
+        users() {
+            const parent = { type: 'courses', id: this.context.id };
+            const relationship = 'memberships';
+            const memberships = this.relatedCourseMemberships({ parent, relationship });
+
+            return (
+                memberships?.map((membership) => {
+                    const parent = { type: membership.type, id: membership.id };
+                    const member = this.relatedUser({ parent, relationship: 'user' });
+
+                    return {
+                        user_id: member.id,
+                        formattedname: member.attributes['formatted-name'],
+                        username: member.attributes['username'],
+                        perm: membership.attributes['permission'],
+                    };
+                }) ?? []
+            );
+        },
+        groups() {
+            const parent = { type: 'courses', id: this.context.id };
+            const relationship = 'status-groups';
+            const statusGroups = this.relatedCourseStatusGroups({ parent, relationship });
+
+            return (
+                statusGroups?.map((statusGroup) => {
+                    return {
+                        id: statusGroup.id,
+                        name: statusGroup.attributes['name'],
+                    };
+                }) ?? []
+            );
+        },
+        autor_members() {
+            if (Object.keys(this.users).length === 0 && this.users.constructor === Object) {
+                return [];
+            }
+
+            let members = this.users.filter(function (user) {
+                return user.perm === 'autor';
+            });
+
+            return members;
+        },
+
+        autor_members_filtered() {
+            if (this.autor_members.length === 0) {
+                return [];
+            }
+            let start = this.autorOffset * this.entries_per_page;
+            let end = ((this.autorOffset + 1) * this.entries_per_page);
+            return this.autor_members.slice(start, end);
+        },
+
+        user_members() {
+            if (Object.keys(this.users).length === 0 && this.users.constructor === Object) {
+                return [];
+            }
+
+            let members = this.users.filter(function (user) {
+                return user.perm === 'user';
+            });
+
+            return members;
+        },
+
+        user_members_filtered() {
+            if (this.user_members.length === 0) {
+                return [];
+            }
+            let start = this.userOffset * this.entries_per_page;
+            let end = ((this.userOffset + 1) * this.entries_per_page);
+            return this.user_members.slice(start, end);
+        },
+
+        entries_per_page() {
+            return STUDIP?.config?.ENTRIES_PER_PAGE ?? 0;
+        },
+
+        can_paginate() {
+            return this.entries_per_page > 0;
+        },
+
+        readApproval() {
+            return {
+                all: this.userPermsReadAll,
+                users: this.userPermsReadUsers,
+                groups: this.userPermsReadGroups,
+            };
+        },
+
+        writeApproval() {
+            return {
+                all: this.userPermsWriteAll,
+                users: this.userPermsWriteUsers,
+                groups: this.userPermsWriteGroups,
+            };
+        },
+    },
+
+    methods: {
+        ...mapActions({
+            loadCourseMemberships: 'course-memberships/loadRelated',
+            loadCourseStatusGroups: 'status-groups/loadRelated',
+        }),
+
+        updateAutorOffset(offset) {
+            this.autorOffset = parseInt(offset);
+        },
+
+        updateUserOffset(offset) {
+            this.userOffset = parseInt(offset);
+        },
+
+        handleBulkSelectRead(event, type) {
+            let state = event.target.checked;
+            let list = type === 'autor' ? this.autor_members_filtered : this.user_members_filtered;
+            if (list.length == 0) {
+                return;
+            }
+            if (type === 'autor') {
+                this.bulkSelectAutorRead = state;
+            } else {
+                this.bulkSelectUserRead = state;
+            }
+            for (let user of list) {
+                if (state) { // Add
+                    if (this.userPermsReadUsers.includes(user.user_id) === false) {
+                        this.userPermsReadUsers.push(user.user_id);
+                    }
+                } else { // Remove
+                    if (this.userPermsReadUsers.includes(user.user_id) === true) {
+                        let index = this.userPermsReadUsers.findIndex((perm) => perm == user.user_id);
+                        this.userPermsReadUsers.splice(index, 1);
+                    }
+                }
+            }
+        },
+
+        handleBulkSelectWrite(event) {
+            let state = event.target.checked;
+            let list = this.autor_members_filtered;
+            if (list.length == 0) {
+                return;
+            }
+            this.bulkSelectAutorWrite = state;
+            for (let user of list) {
+                if (state) { // Add
+                    if (this.userPermsWriteUsers.includes(user.user_id) === false) {
+                        this.userPermsWriteUsers.push(user.user_id);
+                    }
+                } else { // Remove
+                    if (this.userPermsWriteUsers.includes(user.user_id) === true) {
+                        let index = this.userPermsWriteUsers.findIndex((perm) => perm == user.user_id);
+                        this.userPermsWriteUsers.splice(index, 1);
+                    }
+                }
+            }
+        },
+
+        handleBulkSelectReadPassive(type) {
+            let bulkState = false;
+            if (type === 'autor' && this.autor_members_filtered?.length > 0) {
+                let currentAutorsIds = this.autor_members_filtered.map((autor) => autor.user_id);
+                if (currentAutorsIds.every((id) => this.userPermsReadUsers.includes(id))) {
+                    bulkState = true;
+                }
+                this.bulkSelectAutorRead = bulkState;
+            }
+            if (type === 'user' && this.user_members_filtered?.length > 0) {
+                let currentUsersIds = this.user_members_filtered.map((user) => user.user_id);
+                if (currentUsersIds.every((id) => this.userPermsReadUsers.includes(id))) {
+                    bulkState = true;
+                }
+                this.bulkSelectUserRead = bulkState;
+            }
+        },
+
+        handleBulkSelectWritePassive() {
+            let bulkState = false;
+            let currentAutorsIds = this.autor_members_filtered.map((autor) => autor.user_id);
+            if (currentAutorsIds.every((id) => this.userPermsWriteUsers.includes(id))) {
+                bulkState = true;
+            }
+            this.bulkSelectAutorWrite = bulkState;
+        }
+    },
+
+    watch: {
+        userPermsReadUsers(newVal, oldVal) {
+            this.handleBulkSelectReadPassive('autor');
+            this.handleBulkSelectReadPassive('user');
+            this.$emit('updateReadApproval', this.readApproval);
+        },
+        userPermsReadGroups(newVal, oldVal) {
+            this.$emit('updateReadApproval', this.readApproval);
+        },
+        userPermsReadAll(newVal, oldVal) {
+            this.$emit('updateReadApproval', this.readApproval);
+            if (newVal === true) {
+                this.userPermsWriteAll = false;
+            }
+        },
+        userPermsWriteUsers(newVal, oldVal) {
+            this.handleBulkSelectWritePassive();
+            this.$emit('updateWriteApproval', this.writeApproval);
+        },
+        userPermsWriteGroups(newVal, oldVal) {
+            this.$emit('updateWriteApproval', this.writeApproval);
+        },
+        userPermsWriteAll(newVal, oldVal) {
+            this.$emit('updateWriteApproval', this.writeApproval);
+            if (newVal === true) {
+                this.userPermsReadAll = false;
+            }
+        },
+        autorOffset(newVal, oldVal) {
+            this.handleBulkSelectReadPassive('autor');
+            this.handleBulkSelectWritePassive();
+        },
+        userOffset(newVal, oldVal) {
+            this.handleBulkSelectReadPassive('user');
+        }
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelector.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelector.vue
new file mode 100644
index 0000000..0e8acda
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelector.vue
@@ -0,0 +1,141 @@
+<template>
+    <ul class="courseware-structural-element-selector" role="radiogroup">
+        <courseware-structural-element-selector-item 
+            v-if="rootElement !== null"
+            :element="rootElement"
+            :siblings="[]"
+            :selectedId="selectedId"
+            :focusedElementId="focusedElementId"
+            :rootId="rootId"
+            :validateAncestors="validateAncestors"
+            :targetId="targetId"
+            :targetAncestors="targetAncestors"
+            :selectablePurposes="selectablePurposes"
+            @input="handleInput"
+            @focus="handleFocus"
+        />
+    </ul>
+</template>
+
+<script>
+import CoursewareStructuralElementSelectorItem from './CoursewareStructuralElementSelectorItem.vue';
+import { mapActions, mapGetters } from 'vuex'
+
+export default {
+    name: 'courseware-structural-element-selector',
+    components: {
+        CoursewareStructuralElementSelectorItem
+    },
+    model: {
+        prop: 'element'
+    },
+    props: {
+        element: {
+            type: Object
+        },
+        rootId: {
+            type: String,
+            required: true
+        },  
+        validateAncestors: {
+            type: Boolean,
+            default: false
+        },
+        targetId: {
+            type: String,
+            default: null
+        },
+        selectablePurposes: {
+            type: Array,
+            default: () => []
+        }
+    },
+    data() {
+        return {
+            rootElement: null,
+            focusedElementId: '' 
+        };
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            coursewareUnits: 'courseware-units/all',
+            structuralElementById: 'courseware-structural-elements/byId',
+            context: 'context',
+            childrenById: 'courseware-structure/children',
+            currentElement: 'currentElement'
+        }),
+        children() {
+            if (!this.rootElement) {
+                return [];
+            }
+
+            return this.childrenById(this.rootElement.id)
+                .map((id) => this.structuralElementById({ id }))
+                .filter(Boolean);
+        },
+        selectedId() {
+            return this.element?.id ?? '';
+        },
+        targetElement() {
+            return this.structuralElementById({ id: this.targetId });
+        },
+        targetAncestors() {
+            if (!this.targetElement || !this.validateAncestors) {
+                return [];
+            }
+
+            const finder = (parent) => {
+                const parentId = parent.relationships?.parent?.data?.id;
+                if (!parentId) {
+                    return null;
+                }
+                const element = this.structuralElementById({ id: parentId });
+                if (!element) {
+                    console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`);
+                }
+
+                return element;
+            };
+
+            const visitAncestors = function* (node) {
+                const parent = finder(node);
+                if (parent) {
+                    yield parent;
+                    yield* visitAncestors(parent);
+                }
+            };
+
+            return [...visitAncestors(this.targetElement)].reverse();
+        },
+    },
+    methods: {
+        ...mapActions({
+            loadStructuralElement: 'courseware-structural-elements/loadById',
+            companionError: 'companionError',
+            companionSuccess: 'companionSuccess',
+        }),
+        handleInput(id) {
+            this.$emit('input', this.structuralElementById({ id }));
+            this.focusedElementId = id;
+        },
+        handleFocus(id) {
+            this.focusedElementId = id;
+        },
+        async loadRootElement(rootId) {
+            this.rootElement = null;
+            await this.loadStructuralElement({id: rootId, options: {include: 'children'}});
+            this.rootElement = this.structuralElementById({ id: rootId});
+        }
+    },
+    async mounted() {
+        await this.loadRootElement(this.rootId);
+    },
+    watch: {
+        async rootId(newRootId) {
+            await this.loadRootElement(newRootId);
+            this.focusedElementId = '';
+        }
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelectorItem.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelectorItem.vue
new file mode 100644
index 0000000..f6b3cf6
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelectorItem.vue
@@ -0,0 +1,272 @@
+<template>
+    <li class="courseware-structural-element-selector-item">
+        <span
+            class="radiobutton"
+            :tabindex="tabindex"
+            :ref="'radiobutton-' + element.id"
+            role="radio"
+            :aria-checked="selected ? 'true' : 'false'"
+            :aria-labelledby="labelId"
+            @click="handleClickInput(element.id)"
+            @keydown="handleKeyInput($event, element.id)"
+        >
+            <template v-if="selectable">
+                <studip-icon v-if="selected" shape="radiobutton-checked" />
+                <studip-icon v-else shape="radiobutton-unchecked" />
+            </template>
+            <studip-icon v-else shape="decline" role="inactive" />    
+        </span>
+        <template v-if="hasChildren">
+            <a href="#" :aria-expanded="isOpen ? 'true' : 'false'" @click.prevent="toggleChildrenVisibility">
+                <studip-icon v-if="!isOpen" shape="arr_1right" />
+                <studip-icon v-if="isOpen" shape="arr_1down" />
+                <label :id="labelId">
+                    {{ element.attributes.title }}
+                    <span v-if="!selectable" class="sr-only">{{ $gettext('nicht wählbar') }}</span>
+                </label>
+            </a>
+            <ul v-if="isOpen">
+                <courseware-structural-element-selector-item
+                    v-for="child in children"
+                    :key="child.id"
+                    :element="child"
+                    :siblings="children"
+                    :selectedId="selectedId"
+                    :focusedElementId="focusedElementId"
+                    :rootId="rootId"
+                    :validateAncestors="validateAncestors"
+                    :targetId="targetId"
+                    :targetAncestors="targetAncestors"
+                    :selectablePurposes="selectablePurposes"
+                    @input="handleInput"
+                    @focus="handleFocus"
+                    @selectable="updateSelectable"
+                />
+            </ul>
+        </template>
+        <template v-else>
+            <studip-icon shape="arr_1right" role="inactive" class="inactive"/>
+            <label :id="labelId">
+                {{ element.attributes.title }}
+                <span v-if="!selectable" class="sr-only">{{ $gettext('nicht wählbar') }}</span>
+            </label>
+        </template>
+    </li>
+</template>
+<script>
+import { mapActions, mapGetters } from 'vuex'
+
+export default {
+    name: 'courseware-structural-element-selector-item',
+    props: {
+        element: {
+            type: Object
+        },
+        siblings: {
+            type: Array
+        },
+        selectedId: {
+            type: String,
+            required: true
+        },
+        focusedElementId: {
+            type: String
+        },
+        rootId: {
+            type: String,
+            required: true
+        },  
+        validateAncestors: {
+            type: Boolean,
+            default: false
+        },
+        targetId: {
+            type: String,
+            default: null
+        },
+        targetAncestors: {
+            type: Array
+        },
+        selectablePurposes: {
+            type: Array
+        }
+    },
+    data() {
+        return {
+            isOpen: false,
+            selectable: true,
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            coursewareUnits: 'courseware-units/all',
+            structuralElementById: 'courseware-structural-elements/byId',
+            context: 'context',
+            currentElement: 'currentElement'
+        }),
+        children() {
+            const children = this.element?.relationships?.children?.data?.map(child => child.id);
+            if (!children) {
+                return [];
+            }
+
+            return children.map((id) => this.structuralElementById({ id })).filter(Boolean);
+        },
+        hasChildren() {
+            return this.children.length > 0;
+        },
+        selected() {
+            return this.selectedId === this.element?.id;
+        },
+        focused() {
+            return this.focusedElementId === this.element?.id;
+        },
+        labelId() {
+            return this.element.id + '_checkbox-label';
+        },
+        isRoot() {
+            return this.rootId === this.element.id;
+        },
+        tabindex() {
+            if (this.focusedElementId !== '') {
+                return this.focused ? 0 : -1;
+            }
+            return this.isRoot ? 0 : -1;
+        },
+        nextElementId() {
+            if (this.hasChildren && this.isOpen) {
+                return this.children[0].id;
+            }
+
+            return this.nextSiblingId;
+        },
+        nextSiblingId() {
+            if (this.isRoot) {
+                return null;
+            }
+            const index = this.siblings.findIndex(element => element.id === this.element.id) + 1;
+            if (this.siblings.length > index) {
+                return this.siblings[index].id;
+            } else {
+                return this.$parent.nextSiblingId;
+            }
+        },
+        previousElementId() {
+            if (this.isRoot) {
+                return null;
+            }
+            const index = this.siblings.findIndex(element => element.id === this.element.id) - 1;
+            if (index > -1) {
+                const childrenCount = this.siblings[index].relationships.children.data.length;
+                const previousElement = this.$parent.$children.find(child => child.element?.id === this.siblings[index].id);
+                if (childrenCount > 0 && previousElement.isOpen) {
+                        const element = this.structuralElementById({ id: this.siblings[index].relationships.children.data[childrenCount - 1].id });
+                        if (
+                            element.relationships.children.data.length > 0
+                            && previousElement.$children.find(child => child.element?.id === element.id).isOpen
+                        ) {
+                            return element.relationships.children.data[element.relationships.children.data.length -1].id;
+                        }
+                        return element.id;
+                } else {
+                    return this.siblings[index].id;
+                } 
+            } else {
+                return this.$parent.element.id;
+            }
+        },
+    },
+    methods: {
+        ...mapActions({
+            loadStructuralElement: 'courseware-structural-elements/loadById',
+            companionError: 'companionError',
+            companionSuccess: 'companionSuccess',
+        }),
+        loadChildren() {
+            const children = this.element?.relationships?.children?.data?.map(child => child.id) ?? [];
+            children.forEach((id) => this.loadStructuralElement({id: id,  options: {include: 'children'}}));
+        },
+        toggleChildrenVisibility() {
+            if (!this.isOpen) {
+                this.loadChildren();
+            }
+            this.isOpen = !this.isOpen;
+        },
+        handleInput(id) {
+            this.$emit('input', id);
+        },
+        handleFocus(id) {
+            this.$emit('focus', id);
+        },
+        validate() {
+            if (
+                this.element.id === this.targetId
+                || this.targetAncestors.find(ancestor => ancestor.id === this.element.id)
+            ) {
+                this.selectable = false;
+                this.$emit('selectable', false);
+            }
+        },
+        updateSelectable() {
+            this.selectable = false;
+            this.$emit('selectable', false);
+        },
+        filterSelectablePurposes() {
+            if (this.selectablePurposes.length === 0) {
+                return;
+            }
+            this.selectable = this.selectablePurposes.includes(this.element.attributes.purpose);
+        },
+        handleClickInput(id) {
+            if (this.selectable) {
+                this.handleInput(id);
+            }
+        },
+        handleKeyInput(event, id) {
+            switch(event.keyCode) {
+                case 37: // arrow left
+                case 38: // arrow up
+                    event.preventDefault();
+                    if (this.previousElementId !== null) {
+                        this.$emit('focus', this.previousElementId);    
+                    }
+                    break;
+                case 39: // arrow right
+                case 40: // arrow down
+                    event.preventDefault();
+                    if (this.nextElementId !== null) {
+                        this.$emit('focus', this.nextElementId);
+                    }
+                    break;
+            }
+        },
+        selectRoot() {
+            if (this.focusedElementId === '' && this.isRoot && this.selectable) {
+                this.handleInput(this.element.id);
+            }
+        }
+    },
+    mounted() {
+        this.loadChildren();
+        if (this.validateAncestors) {
+            this.validate();
+        }
+        this.filterSelectablePurposes();
+        this.selectRoot();
+    },
+    watch: {
+        focusedElementId(newId) {
+            if (this.focused) {
+                this.$refs['radiobutton-'+ this.element.id].focus();
+                if (this.selectable) {
+                    this.handleInput(newId);
+                } else {
+                    this.handleInput('');
+                }
+            }
+            this.selectRoot();
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareToolsBlockadder.vue b/resources/vue/components/courseware/structural-element/CoursewareToolsBlockadder.vue
new file mode 100644
index 0000000..86b4246
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareToolsBlockadder.vue
@@ -0,0 +1,436 @@
+<template>
+    <div class="cw-tools-element-adder">
+        <courseware-tabs class="cw-tools-element-adder-tabs">
+            <courseware-tab :name="$gettext('Blöcke')" :selected="showBlockadder" :index="0" :style="{ maxHeight: maxHeight + 'px' }">
+                <form @submit.prevent="loadSearch">
+                    <div class="input-group files-search search cw-block-search">
+                        <input
+                            ref="searchBox"
+                            type="text"
+                            v-model="searchInput"
+                            @click.stop
+                            :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')"
+                        />
+                        <span class="input-group-append" @click.stop>
+                            <button v-if="searchInput"
+                                type="button"
+                                class="button reset-search"
+                                id="reset-search"
+                                :title="$gettext('Suche zurücksetzen')"
+                                @click="resetSearch"
+                            >
+                                <studip-icon shape="decline" :size="20"></studip-icon>
+                            </button>
+                            <button 
+                                type="submit"
+                                class="button"
+                                id="search-btn"
+                                :title="$gettext('Suche starten')"
+                                @click="loadSearch"
+                            >
+                                <studip-icon shape="search" :size="20"></studip-icon>
+                            </button>
+                        </span>
+                    </div>
+                </form>
+
+                <div class="filterpanel">
+                    <span class="sr-only">{{ $gettext('Kategorien Filter') }}</span>
+                    <button
+                        v-for="category in blockCategories"
+                        :key="category.type"
+                        class="button"
+                        :class="{'button-active': category.type ===  currentFilterCategory }"
+                        :aria-pressed="category.type ===  currentFilterCategory ? 'true' : 'false'"
+                        @click="selectCategory(category.type)"
+                    >
+                        {{ category.title }}
+                    </button>
+                </div>
+
+                <div v-if="filteredBlockTypes.length > 0" class="cw-blockadder-item-list">
+                    <courseware-blockadder-item
+                        v-for="(block, index) in filteredBlockTypes"
+                        :key="index"
+                        :title="block.title"
+                        :type="block.type"
+                        :description="block.description"
+                        @blockAdded="$emit('blockAdded')"
+                    />
+                </div>
+                <courseware-companion-box
+                    v-else
+                    :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')"
+                    mood="pointing"
+                />
+            </courseware-tab>
+            <courseware-tab :name="$gettext('Abschnitte')" :selected="showContaineradder" :index="1" :style="{ maxHeight: maxHeight + 'px' }">
+                <div class="cw-container-style-selector" role="group" aria-labelledby="cw-containeradder-style">
+                    <p class="sr-only" id="cw-containeradder-style">{{ $gettext('Abschnitt-Stil') }}</p>
+                    <template
+                        v-for="style in containerStyles"
+                    >
+                        <input
+                            :key="style.key  + '-input'"
+                            type="radio"
+                            name="container-style"
+                            :id="'style-' + style.colspan"
+                            v-model="selectedContainerStyle"
+                            :value="style.colspan"
+                        />
+                        <label
+                            :key="style.key + '-label'"
+                            :for="'style-' + style.colspan"
+                            :class="[selectedContainerStyle === style.colspan ? 'cw-container-style-selector-active' : '', style.colspan]"
+                        >
+                            {{ style.title }}
+                        </label>
+                        
+                    </template>
+                </div>
+                <courseware-container-adder-item
+                    v-for="container in containerTypes"
+                    :key="container.type"
+                    :title="container.title"
+                    :type="container.type"
+                    :colspan="selectedContainerStyle"
+                    :description="container.description"
+                    :firstSection="$gettext('erstes Element')"
+                    :secondSection="$gettext('zweites Element')"
+                ></courseware-container-adder-item>
+            </courseware-tab>
+            <courseware-tab :name="$gettext('Merkliste')" :selected="showClipboard" :index="2" :style="{ maxHeight: maxHeight + 'px' }">
+                <courseware-collapsible-box :title="$gettext('Blöcke')" :open="clipboardBlocks.length > 0">
+                    <template v-if="clipboardBlocks.length > 0">
+                        <div class="cw-element-inserter-wrapper">
+                            <courseware-clipboard-item
+                                v-for="(clipboard, index) in clipboardBlocks"
+                                :key="index"
+                                :clipboard="clipboard"
+                                @inserted="$emit('blockAdded')"
+                            />
+                        </div>
+                        <button class="button trash" @click="clearClipboard('courseware-blocks')">
+                            {{ $gettext('Alle Blöcke aus Merkliste entfernen') }}
+                        </button>
+                    </template>
+                    <courseware-companion-box
+                        v-else
+                        mood="pointing"
+                        :msgCompanion="$gettext('Die Merkliste enthält keine Blöcke.')"
+                    />
+                </courseware-collapsible-box>
+                <courseware-collapsible-box :title="$gettext('Abschnitte')" :open="clipboardContainers.length > 0">
+                    <template v-if="clipboardContainers.length > 0">
+                        <div class="cw-element-inserter-wrapper">
+                            <courseware-clipboard-item
+                                v-for="(clipboard, index) in clipboardContainers"
+                                :key="index"
+                                :clipboard="clipboard"
+                            />
+                        </div>
+                        <button class="button trash" @click="clearClipboard('courseware-containers')">
+                            {{ $gettext('Alle Abschnitte aus Merkliste entfernen') }}
+                        </button>
+                    </template>
+                    <courseware-companion-box
+                        v-else
+                        mood="pointing"
+                        :msgCompanion="$gettext('Die Merkliste enthält keine Abschnitte.')"
+                    />
+                </courseware-collapsible-box>
+            </courseware-tab>
+        </courseware-tabs>
+        <studip-dialog
+            v-if="showDeleteClipboardDialog"
+            :title="textDeleteClipboardTitle"
+            :question="textDeleteClipboardAlert"
+            height="200"
+            width="500"
+            @confirm="executeDeleteClipboard"
+            @close="closeDeleteClipboardDialog"
+        ></studip-dialog>
+    </div>
+</template>
+
+<script>
+import CoursewareTabs from '../layouts/CoursewareTabs.vue';
+import CoursewareTab from '../layouts/CoursewareTab.vue';
+import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue';
+import CoursewareClipboardItem from './CoursewareClipboardItem.vue';
+import CoursewareContainerAdderItem from '../containers/CoursewareContainerAdderItem.vue';
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue';
+import StudipDialog from '../../StudipDialog.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'cw-tools-blockadder',
+    components: {
+        CoursewareTabs,
+        CoursewareTab,
+        CoursewareBlockadderItem,
+        CoursewareClipboardItem,
+        CoursewareContainerAdderItem,
+        CoursewareCompanionBox,
+        CoursewareCollapsibleBox,
+
+        StudipDialog,
+    },
+    props: {
+        stickyRibbon: {
+            type: Boolean,
+            default: false,
+        },
+    },
+    data() {
+        return {
+            showBlockadder: true,
+            showContaineradder: false,
+            showClipboard: false,
+            searchInput: '',
+            currentFilterCategory: '',
+            filteredBlockTypes: [],
+            categorizedBlocks: [],
+            selectedContainerStyle: 'full',
+            showDeleteClipboardDialog: false,
+            deleteClipboardType: null
+        };
+    },
+    computed: {
+        ...mapGetters({
+            adderStorage: 'blockAdder',
+            containerAdder: 'containerAdder',
+            unorderedBlockTypes: 'blockTypes',
+            containerTypes: 'containerTypes',
+            favoriteBlockTypes: 'favoriteBlockTypes',
+            showToolbar: 'showToolbar',
+            usersClipboards: 'courseware-clipboards/all',
+            userId: 'userId'
+        }),
+        blockTypes() {
+            let blockTypes = JSON.parse(JSON.stringify(this.unorderedBlockTypes));
+            blockTypes.sort((a, b) => {
+                return a.title > b.title ? 1 : b.title > a.title ? -1 : 0;
+            });
+            return blockTypes;
+        },
+        containerStyles() {
+            return [
+                { key: 0, title: this.$gettext('Volle Breite'), colspan: 'full'},
+                { key: 1, title: this.$gettext('Halbe Breite'), colspan: 'half' },
+                { key: 2, title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' },
+            ];
+        },
+        blockCategories() {
+            return [
+                { title: this.$gettext('Favoriten'), type: 'favorite' },
+                { title: this.$gettext('Texte'), type: 'text' },
+                { title: this.$gettext('Multimedia'), type: 'multimedia' },
+                { title: this.$gettext('Interaktion'), type: 'interaction' },
+                { title: this.$gettext('Gestaltung'), type: 'layout' },
+                { title: this.$gettext('Externe Inhalte'), type: 'external' },
+                { title: this.$gettext('Biografie'), type: 'biography' },
+            ];
+        },
+        maxHeight() {
+            if (this.stickyRibbon) {
+                return parseInt(window.innerHeight * 0.75) - 120;
+            } else {
+                return parseInt(Math.min(window.innerHeight * 0.75, window.innerHeight - 197)) - 120;
+            }
+        },
+        clipboardBlocks() {
+            return this.usersClipboards
+                .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-blocks')
+                .sort((a, b) => b.attributes.mkdate - a.attributes.mkdate);
+        },
+        clipboardContainers() {
+            return this.usersClipboards
+                .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-containers')
+                .sort((a, b) => b.attributes.mkdate < a.attributes.mkdate);
+        },
+        textDeleteClipboardTitle() {
+            if (this.deleteClipboardType === 'courseware-blocks') {
+                return this.$gettext('Merkliste für Blöcke leeren');
+            }
+            if (this.deleteClipboardType === 'courseware-containers') {
+                return this.$gettext('Merkliste für Abschnitte leeren');
+            }
+            return '';
+        },
+        textDeleteClipboardAlert() {
+            if (this.deleteClipboardType === 'courseware-blocks') {
+                return this.$gettext('Möchten Sie die Merkliste für Blöcke unwiderruflich leeren?');
+            }
+            if (this.deleteClipboardType === 'courseware-containers') {
+                return this.$gettext('Möchten Sie die Merkliste für Abschnitte unwiderruflich leeren?');
+            }
+            return '';
+        }
+    },
+    methods: {
+        ...mapActions({
+            removeFavoriteBlockType: 'removeFavoriteBlockType',
+            addFavoriteBlockType: 'addFavoriteBlockType',
+            coursewareContainerAdder: 'coursewareContainerAdder',
+            companionWarning: 'companionWarning',
+            deleteUserClipboards: 'deleteUserClipboards'
+        }),
+        displayContainerAdder() {
+            this.showContaineradder = true;
+            this.showBlockadder = false;
+            this.showClipboard = false;
+        },
+        displayBlockAdder() {
+            this.showContaineradder = false;
+            this.showClipboard = false;
+            this.showBlockadder = true;
+            this.disableContainerAdder();
+        },
+        displayClipboard() {
+            this.showClipboard = true;
+        },
+        isBlockFav(block) {
+            let isFav = false;
+            this.favoriteBlockTypes.forEach((type) => {
+                if (type.type === block.type) {
+                    isFav = true;
+                }
+            });
+
+            return isFav;
+        },
+        disableContainerAdder() {
+            this.coursewareContainerAdder(false);
+        },
+        loadSearch() {
+            let searchTerms = this.searchInput.trim();
+            if (searchTerms.length < 3 && !this.currentFilterCategory) {
+                this.companionWarning({info: this.$gettext('Leider ist Ihr Suchbegriff zu kurz. Der Suchbegriff muss mindestens 3 Zeichen lang sein.')});
+                return;
+            }
+            this.filteredBlockTypes = this.blockTypes;
+
+            // filter results by given filter first so only these results are searched if an additional search term is given
+            if (this.currentFilterCategory) {
+                this.filterBlockTypesByCategory();
+                this.categorizedBlocks = this.filteredBlockTypes;
+            } else {
+                this.categorizedBlocks = this.blockTypes;
+            }
+
+            searchTerms = searchTerms.toLowerCase().split(' ');
+
+            // sort out block types that don't contain all search words
+            searchTerms.forEach(term => {
+                this.filteredBlockTypes = this.filteredBlockTypes.filter(block => (
+                    block.title.toLowerCase().includes(term)
+                    || block.description.toLowerCase().includes(term)
+                ));
+            });
+
+            // add block types to the search if a search term matches a tag even if they aren't in the given category
+            if (this.searchInput.trim().length > 0) {
+                this.filteredBlockTypes.push(...this.getBlockTypesByTags(searchTerms));
+                // remove possible duplicates
+                this.filteredBlockTypes = [...new Map(this.filteredBlockTypes.map(item => [item['title'], item])).values()];
+            }
+        },
+        filterBlockTypesByCategory() {
+            if (this.currentFilterCategory !== 'favorite') {
+                this.filteredBlockTypes = this.filteredBlockTypes.filter(block => block.categories.includes(this.currentFilterCategory));
+            } else {
+                this.filteredBlockTypes = this.favoriteBlockTypes;
+            }
+            
+        },
+        getBlockTypesByTags(searchTags) {
+            return this.categorizedBlocks.filter(block => {
+                const lowercaseTags = block.tags.map(blockTag => blockTag.toLowerCase());
+                for (const tag of searchTags) {
+                    if (lowercaseTags.filter(blockTag => blockTag.includes(tag.toLowerCase())).length > 0) {
+                        return true;
+                    }
+                }
+                return false;
+            });
+        },
+        selectCategory(type) {
+            if (this.currentFilterCategory !== type) {
+                this.currentFilterCategory = type;
+            } else {
+                this.resetCategory();
+            }
+        },
+        resetCategory() {
+            this.currentFilterCategory = '';
+            if (!this.searchInput) {
+                this.filteredBlockTypes = this.blockTypes;
+            } else {
+                this.loadSearch();
+            }
+        },
+        resetSearch() {
+            this.filteredBlockTypes = this.blockTypes;
+            this.searchInput = '';
+            this.currentFilterCategory = '';
+        },
+        clearClipboard(type) {
+            this.deleteClipboardType = type;
+            this.showDeleteClipboardDialog = true;
+        },
+        executeDeleteClipboard() {
+            if (this.deleteClipboardType) {
+                this.deleteUserClipboards({uid: this.userId, type: this.deleteClipboardType});
+            }
+            this.closeDeleteClipboardDialog();
+        },
+        closeDeleteClipboardDialog() {
+            this.showDeleteClipboardDialog = false;
+            this.deleteClipboardType = null;
+        }
+    },
+    mounted() {
+        if (this.containerAdder === true) {
+            this.displayContainerAdder();
+        }
+        this.filteredBlockTypes = this.blockTypes;
+        setTimeout(() => this.$refs.searchBox.focus(), 800);
+    },
+    watch: {
+        adderStorage(newValue) {
+            if (Object.keys(newValue).length !== 0) {
+                this.displayBlockAdder();
+            }
+        },
+        containerAdder(newValue) {
+            if (newValue === true) {
+                this.displayContainerAdder();
+            }
+        },
+        showToolbar(newValue, oldValue) {
+            if (oldValue === true && newValue === false) {
+                this.disableContainerAdder();
+            }
+        },
+        searchInput(newValue, oldValue) {
+            if (newValue.length >= 3 && newValue !== oldValue) {
+                this.loadSearch();
+            }
+            if (newValue.length < oldValue.length && newValue.length < 3) {
+                if (!this.currentFilterCategory) {
+                    this.filteredBlockTypes = this.blockTypes;
+                } else {
+                    this.loadSearch();
+                }
+            }
+        },
+        currentFilterCategory(newValue) {
+            if (newValue) {
+                this.loadSearch();
+            }
+        }
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue b/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue
new file mode 100644
index 0000000..406045e
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue
@@ -0,0 +1,23 @@
+<template>
+    <div class="cw-tools cw-tools-contents">
+        <courseware-tree v-if="structuralElements.length" />
+    </div>
+</template>
+
+<script>
+import CoursewareTree from './CoursewareTree.vue';
+import { mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-tools-contents',
+    components: {
+        CoursewareTree,
+    },
+
+    computed: {
+        ...mapGetters({
+            structuralElements: 'courseware-structural-elements/all',
+        }),
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareTree.vue b/resources/vue/components/courseware/structural-element/CoursewareTree.vue
new file mode 100644
index 0000000..4103c1b
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareTree.vue
@@ -0,0 +1,248 @@
+<template>
+    <div class="cw-tree" ref="tree">
+        <template v-if="editMode">
+                <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
+                <span id="operation" class="assistive-text">
+                    {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
+                </span>
+        </template>
+        <ol v-if="!processing" class="cw-tree-root-list" role="listbox">
+            <courseware-tree-item
+                class="cw-tree-item"
+                :element="rootElementWithNestedChildren"
+                :currentElement="currentElement"
+                @sort="sort"
+                @moveItemUp="moveItemUp"
+                @moveItemDown="moveItemDown"
+                @moveItemPrevLevel="moveItemPrevLevel"
+                @moveItemNextLevel="moveItemNextLevel"
+                @childrenUpdated="updateNestedChildren"
+            ></courseware-tree-item>
+        </ol>
+        <studip-progress-indicator
+            v-else 
+            :description="$gettext('Vorgang wird bearbeitet...')"
+        />
+    </div>
+</template>
+
+<script>
+import CoursewareTreeItem from './CoursewareTreeItem.vue';
+import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    components: { 
+        CoursewareTreeItem,
+        StudipProgressIndicator
+    },
+    name: 'courseware-tree',
+    data() {
+        return {
+            nestedChildren: [],
+            processing: false,
+            rootElementWithNestedChildren: {},
+        }
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            courseware: 'courseware',
+            relatedStructuralElement: 'courseware-structural-elements/related',
+            structuralElementById: 'courseware-structural-elements/byId',
+            childrenById: 'courseware-structure/children',
+            viewMode: 'viewMode',   
+            structuralElements: 'courseware-structural-elements/all',
+            assistiveLive: 'assistiveLiveContents'
+        }),
+        currentElement() {
+            const id = this.$route?.params?.id;
+            if (!id) {
+                return null;
+            }
+
+            return this.structuralElementById({ id }) ?? null;
+        },
+
+        rootElement() {
+            if (this.context.type !== 'public') {
+                    const root = this.relatedStructuralElement({
+                    parent: { id: this.courseware.id, type: this.courseware.type },
+                    relationship: 'root',
+                });
+
+                return root;
+            } else {
+                return this.structuralElementById({ id: this.context.rootId });
+            }
+
+        },
+        editMode() {
+            return this.viewMode === 'edit';
+        },
+    },
+    methods: {
+         ...mapActions({
+            updateStructuralElement: 'updateStructuralElement',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+            sortChildrenInStructualElements: 'sortChildrenInStructualElements',
+            loadStructuralElement: 'loadStructuralElement',
+            setAssistiveLiveContents: 'setAssistiveLiveContents',
+            companionError: 'companionError',
+         }),
+        updateNestedChildren() {
+            this.nestedChildren = this.getNestedChildren(this.rootElement);
+            this.setRootElementWithNestedChildren();
+        },
+        setRootElementWithNestedChildren() {
+            let element = { id: this.rootElement.id, attributes: this.rootElement.attributes };
+            element.nestedChildren = this.nestedChildren;
+
+            this.rootElementWithNestedChildren = element;
+        },
+        getNestedChildren(structuralElement) {
+            let children = _.cloneDeep(this.structuralElements
+                .filter(
+                    element => element.relationships.parent?.data?.id === structuralElement.id
+                )).sort((a,b) => a.attributes.position - b.attributes.position);
+
+            let nestedChildren = [];
+            for (let child of children) {
+                child.nestedChildren = this.getNestedChildren(child);
+                nestedChildren.push(child);
+            }
+
+            return nestedChildren;
+        },
+        async sort(data) {
+            const tree = this.$refs.tree;
+            const currentScrollPosition = tree.offsetParent.scrollTop;
+            this.processing = true;
+            await this.loadStructuralElement(data.newParent);
+            const newParent = this.structuralElementById({ id: data.newParent });
+            if (!newParent.attributes['can-edit']) {
+                this.processing = false;
+                this.updateNestedChildren();
+                this.companionError({
+                    info: this.$gettext('Verschieben nicht möglich. Sie haben keine Schreibrechte für die Ziel-Seite.')
+                });
+                return;
+            }
+            if (data.oldParent !== data.newParent) {
+                await this.lockObject({ id: data.id, type: 'courseware-structural-elements' });
+                let element = this.structuralElementById({ id: data.id });
+                element.relationships.parent.data.id = newParent.id;
+                await this.updateStructuralElement({
+                    element: element,
+                    id: element.id,
+                });
+                await this.unlockObject({ id: data.id, type: 'courseware-structural-elements' });
+                await this.loadStructuralElement(data.id);
+            }
+            await this.loadStructuralElement(newParent.id);
+            const parent = this.structuralElementById({ id: newParent.id });
+            await this.sortChildrenInStructualElements({parent: parent, children: data.sortArray});
+            this.updateNestedChildren();
+            this.processing = false;
+            this.$nextTick(() => {
+                tree.offsetParent.scrollTop = currentScrollPosition;
+            });
+        },
+        moveItemUp(data) {
+            data.direction = 'up';
+            this.reorderNestedChildren(data);
+        },
+        moveItemDown(data) {
+            data.direction = 'down';
+            this.reorderNestedChildren(data);
+        },
+        moveItemPrevLevel(data) {
+            data.direction = 'prev';
+            this.reorderNestedChildren(data);
+        },
+        moveItemNextLevel(data) {
+            data.direction = 'next';
+            this.reorderNestedChildren(data);
+        },
+        reorderNestedChildren(data) {
+            this.rootElementWithNestedChildren = this.recursiveNestedChildrenUpdate(this.rootElementWithNestedChildren, data);
+        },
+        recursiveNestedChildrenUpdate(element, data) {
+            if (data.direction === 'prev' && data.parents[1] && element.id === data.parents[1]) {
+                //element is grandparent
+                let parentIndex = element.nestedChildren.findIndex((e) => e.id === data.parents[0]);
+                let movingElementIndex = element.nestedChildren[parentIndex].nestedChildren.findIndex((e) => e.id === data.element.id);
+                const newPos = parentIndex + 1;
+                element.nestedChildren.splice(newPos, 0, element.nestedChildren[parentIndex].nestedChildren[movingElementIndex]);
+                element.nestedChildren[parentIndex].nestedChildren.splice(movingElementIndex, 1);
+
+                element.nestedChildren[newPos].newPos = newPos;
+                element.nestedChildren[newPos].newParentId =  parseInt(element.id);
+                element.nestedChildren[newPos].sortArray = element.nestedChildren.map(c => {return {id: c.id, type: 'courseware-structural-elements'}});
+                element.nestedChildren[newPos].moveDirection = data.direction;
+
+                const assistiveLive = this.$gettextInterpolate(
+                    this.$gettext('%{elementTitle} eine Ebene nach oben bewegt. Übergeordnete Seite: %{parentTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}'), 
+                    { elementTitle: data.element.attributes.title, parentTitle: element.attributes.title, pos: newPos + 1, listLength: element.nestedChildren[newPos].sortArray.length }
+                );
+                this.setAssistiveLiveContents(assistiveLive);
+            }
+            if (element.id === data.parents[0]) {
+                if (data.direction === 'up' || data.direction === 'down') {
+                    const elementIndex = element.nestedChildren.findIndex((e) => e.id === data.element.id);
+                    let vertical = data.direction === 'up' ? -1 : data.direction === 'down' ? 1 : 0;
+                    const newPos = elementIndex + vertical;
+                    if (newPos >= 0 && newPos < element.nestedChildren.length) {
+                        element.nestedChildren.splice(newPos, 0, element.nestedChildren.splice(elementIndex, 1)[0]);
+                        element.nestedChildren[newPos].newPos = newPos;
+                        element.nestedChildren[newPos].newParentId = parseInt(element.id);
+                        element.nestedChildren[newPos].sortArray = element.nestedChildren.map(c => {return {id: c.id, type: 'courseware-structural-elements'}});
+                        element.nestedChildren[newPos].moveDirection = data.direction;
+
+                        const assistiveLive = this.$gettextInterpolate(
+                            this.$gettext('%{elementTitle} bewegt. Aktuelle Position in der Liste: %{pos} von %{listLength}'), 
+                            { elementTitle: data.element.attributes.title, pos: newPos + 1, listLength: element.nestedChildren[newPos].sortArray.length }
+                        );
+                        this.setAssistiveLiveContents(assistiveLive);
+                    }
+                }
+                if (data.direction === 'next') {
+                    const elementIndex = element.nestedChildren.findIndex((e) => e.id === data.element.id);
+                    if (elementIndex !== 0) {
+                        const newParentIndex = elementIndex - 1;
+                        element.nestedChildren[newParentIndex].nestedChildren.push(element.nestedChildren[elementIndex]);
+                        element.nestedChildren.splice(elementIndex, 1);
+                        const newPos = element.nestedChildren[newParentIndex].nestedChildren.length - 1;
+                        element.nestedChildren[newParentIndex].nestedChildren[newPos].newPos = newPos;
+                        const newParentId = element.nestedChildren[newParentIndex].id;
+                        element.nestedChildren[newParentIndex].nestedChildren[newPos].newParentId = parseInt(newParentId);
+                        element.nestedChildren[newParentIndex].nestedChildren[newPos].sortArray = element.nestedChildren[newParentIndex].nestedChildren.map(c => {return {id: c.id, type: 'courseware-structural-elements'}});
+
+                        const assistiveLive = this.$gettextInterpolate(
+                            this.$gettext('%{elementTitle} eine Ebene nach unten bewegt. Übergeordnete Seite: %{parentTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}'), 
+                            { elementTitle: data.element.attributes.title, parentTitle: element.nestedChildren[newParentIndex].attributes.title, pos: newPos + 1, listLength: element.nestedChildren[newParentIndex].nestedChildren[newPos].sortArray.length }
+                        );
+                        this.setAssistiveLiveContents(assistiveLive);
+                    }
+                }
+            } else {
+                element.nestedChildren.forEach((child,index) => {
+                    element.nestedChildren[index] = this.recursiveNestedChildrenUpdate(child, data);
+                });
+            }
+
+            return element;
+        },
+    },
+    mounted() {
+        this.updateNestedChildren();
+    },
+    watch: {
+        structuralElements() {
+            this.updateNestedChildren();
+        }
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue b/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue
new file mode 100644
index 0000000..820b58a
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue
@@ -0,0 +1,473 @@
+<template>
+    <li v-if="showItem"
+        :draggable="editMode ? true : null"
+        :aria-selected="editMode ? keyboardSelected : null"
+    >
+        <div class="cw-tree-item-wrapper">
+            <span
+                v-if="editMode && depth > 0 && canEdit"
+                class="cw-sortable-handle"
+                :tabindex="0"
+                aria-describedby="operation"
+                ref="sortableHandle"
+                role="button"
+                @keydown="handleKeyEvent"
+            >
+            </span>
+            <router-link
+                :to="'/structural_element/' + element.id"
+                class="cw-tree-item-link"
+                :class="{
+                    'cw-tree-item-link-current': isCurrent,
+                    'cw-tree-item-link-edit': editMode,
+                    'cw-tree-item-link-selected': keyboardSelected
+                }"
+            >
+                {{ element.attributes?.title || "–" }}
+                <span v-if="task">| {{ solverName }}</span>
+                <span
+                    v-if="hasReleaseOrWithdrawDate"
+                    class="cw-tree-item-flag-date"
+                    :title="$gettext('Diese Seite hat eine zeitlich beschränkte Sichtbarkeit')"
+                ></span>
+                <span
+                    v-if="hasWriteApproval"
+                    class="cw-tree-item-flag-write"
+                    :title="$gettext('Diese Seite kann von Teilnehmenden bearbeitet werden')"
+                ></span>
+                <span
+                    v-if="hasNoReadApproval"
+                    class="cw-tree-item-flag-cant-read"
+                    :title="$gettext('Diese Seite kann von Teilnehmenden nicht gesehen werden')"
+                ></span>
+                <template v-if="!userIsTeacher && inCourse">
+                    <span
+                        v-if="complete"
+                        class="cw-tree-item-sequential cw-tree-item-sequential-complete"
+                        :title="$gettext('Diese Seite wurde von Ihnen vollständig bearbeitet')"
+                    >
+                    </span>
+                    <span
+                        v-else
+                        class="cw-tree-item-sequential cw-tree-item-sequential-percentage"
+                        :title="$gettextInterpolate(
+                                    $gettext('Fortschritt: %{progress}%'),
+                                    {progress: itemProgress}
+                                )"
+                    >
+                        {{ itemProgress }} %
+                    </span>
+            </template>
+            </router-link>
+        </div>
+        <ol
+            v-if="hasChildren && !editMode"
+            :class="{
+                'cw-tree-chapter-list': isRoot,
+                'cw-tree-subchapter-list': isFirstLevel,
+            }"
+        >
+            <courseware-tree-item
+                v-for="child in children"
+                :key="child.id"
+                :element="child"
+                :currentElement="currentElement"
+                :depth="depth + 1"
+                class="cw-tree-item"
+            />
+        </ol>
+        <draggable
+            v-if="editMode"
+            :class="{ 'cw-tree-chapter-list-empty': nestedChildren.length === 0 }"
+            tag="ol"
+            :component-data="draggableData"
+            class="cw-tree-draggable-list"
+            handle=".cw-sortable-handle"
+            v-bind="dragOptions"
+            :elementId="element.id"
+            :list="nestedChildren" 
+            :group="{ name: 'g1' }"
+            @end="endDrag"
+        >
+            <courseware-tree-item 
+                v-for="el in nestedChildren"
+                :key="el.id"
+                :element="el"
+                :currentElement="currentElement"
+                :depth="depth + 1"
+                :newPos="el.newPos"
+                :newParentId="el.newParentId"
+                :siblingCount="nestedChildren.length"
+                class="cw-tree-item"
+                :elementid="el.id"
+                @sort="sort"
+                @moveItemUp="moveItemUp"
+                @moveItemDown="moveItemDown"
+                @moveItemPrevLevel="moveItemPrevLevel"
+                @moveItemNextLevel="moveItemNextLevel"
+                @childrenUpdated="$emit('childrenUpdated')"
+            />
+        </draggable>
+    </li>
+</template>
+
+<script>
+import draggable from 'vuedraggable';
+import { mapGetters, mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-tree-item',
+    components: {
+        draggable
+    },
+    props: {
+        element: {
+            type: Object,
+            required: true,
+        },
+        currentElement: {
+            type: Object,
+        },
+        depth: {
+            type: Number,
+            default: 0,
+        },
+        keyboardSelectedProp: {
+            type: Boolean,
+            default: false
+        },
+        newPos: {
+            type: Number
+        },
+        newParentId: {
+            type: Number
+        },
+        siblingCount: {
+            type: Number
+        }
+    },
+    data() {
+        return {
+            keyboardSelected: false
+        }
+    },
+    computed: {
+        ...mapGetters({
+            childrenById: 'courseware-structure/children',
+            structuralElementById: 'courseware-structural-elements/byId',
+            context: 'context',
+            taskById: 'courseware-tasks/byId',
+            userById: 'users/byId',
+            groupById: 'status-groups/byId',
+            viewMode: 'viewMode',
+            courseware: 'courseware',
+            progressData: 'progresses',
+            userIsTeacher: 'userIsTeacher',
+        }),
+        draggableData() {
+            return {
+                attrs: {
+                    role: 'listbox',
+                    ['aria-label']: this.$gettextInterpolate(this.$gettext('Unterseiten von %{elementName}'),{ elementName: this.element.attributes?.title })
+                }
+            };
+        },
+        children() {
+            if (!this.element) {
+                return [];
+            }
+
+            return this.childrenById(this.element.id)
+                .map((id) => this.structuralElementById({ id }))
+                .filter(Boolean).sort((a,b) => a.attributes.position - b.attributes.position);
+        },
+        nestedChildren() {
+            return this.element.nestedChildren ?? [];
+        },
+        hasChildren() {
+            return this.childrenById(this.element.id).length;
+        },
+        isRoot() {
+            return this.depth === 0;
+        },
+        isFirstLevel() {
+            return this.depth === 1;
+        },
+        isCurrent() {
+            return this.element.id === this.currentElement?.id;
+        },
+        hasReleaseOrWithdrawDate() {
+            return (
+                this.element.attributes?.['release-date'] !== null || this.element.attributes?.['withdraw-date'] !== null
+            );
+        },
+        hasWriteApproval() {
+            const writeApproval = this.element.attributes?.['write-approval'];
+
+            if (!writeApproval || Object.keys(writeApproval).length === 0) {
+                return false;
+            }
+            return (writeApproval.all || writeApproval.groups.length > 0 || writeApproval.users.length > 0) && this.element.attributes?.['can-edit'];
+        },
+        hasNoReadApproval() {
+            if (this.context.type === 'users') {
+                return false;
+            }
+            const readApproval = this.element.attributes?.['read-approval'];
+
+            if (!readApproval || Object.keys(readApproval).length === 0 || this.hasWriteApproval) {
+                return false;
+            }
+            return !readApproval.all && readApproval.groups.length === 0 && readApproval.users.length === 0;
+        },
+        hasPurposeClass() {
+            return this.purposeClass !== '';
+        },
+        purposeClass() {
+            if (
+                (this.isFirstLevel && this.context.type === 'users') ||
+                (this.context.type === 'courses' && this.element.attributes?.purpose === 'task')
+            ) {
+                return this.element.attributes?.purpose;
+            }
+            return '';
+        },
+        task() {
+            if (this.element.relationships?.task?.data) {
+                return this.taskById({
+                    id: this.element.relationships?.task?.data?.id,
+                });
+            }
+
+            return null;
+        },
+        taskProgress() {
+            return this.task ? this.task.attributes.progress + '%' : '';
+        },
+        solver() {
+            if (this.task) {
+                const solver = this.task.relationships.solver.data;
+                if (solver.type === 'users') {
+                    return this.userById({ id: solver.id });
+                }
+                if (solver.type === 'status-groups') {
+                    return this.groupById({ id: solver.id });
+                }
+            }
+
+            return null;
+        },
+        solverName() {
+            if (this.solver) {
+                if (this.solver.type === 'users') {
+                    return this.solver.attributes['formatted-name'];
+                }
+                if (this.solver.type === 'status-groups') {
+                    return this.solver.attributes.name;
+                }
+            }
+
+            return '';
+        },
+        isTask() {
+            return this.element.attributes?.purpose === 'task';
+        },
+        showItem() {
+            if (this.isTask) {
+                return this.task !== undefined;
+            }
+
+            return true;
+        },
+        editMode() {
+            return this.viewMode === 'edit';
+        },
+        dragOptions() {
+            return {
+                animation: 0,
+                disabled: false,
+                ghostClass: "cw-tree-item-ghost"
+            };
+        },
+        canEdit() {
+            return this.element.attributes['can-edit'];
+        },
+        inCourse() {
+            return this.context.type === 'courses';
+        },
+        progress() {
+            return this.progressData?.[this.element.id];
+        },
+        itemProgress() {
+            return this.progress?.progress?.self ?? 0;
+        },
+        complete() {
+            return this.itemProgress === 100;
+        },
+    },
+    methods: {
+        ...mapActions({
+            loadTask: 'loadTask',
+            setAssistiveLiveContents: 'setAssistiveLiveContents'
+        }),
+        endDrag(e) {
+            let sortArray = [];
+            for (let child of e.to.childNodes) {
+                sortArray.push({id: child.attributes.elementid.nodeValue, type: 'courseware-structural-elements'});
+            }
+
+            let data = {
+                id: e.item._underlying_vm_.id,
+                newPos: e.newIndex,
+                oldPos: e.oldIndex,
+                oldParent: e.item._underlying_vm_.relationships.parent.data.id,
+                newParent: e.to.__vue__.$attrs.elementId,
+                sortArray: sortArray
+            };
+
+            if (data.oldParent === data.newParent && data.oldPos === data.newPos) {
+                return;
+            }
+            if (data.oldParent !== data.newParent) {
+                sortArray.splice(data.newPos, 0, {id: data.id, type: 'courseware-structural-elements'});
+            } 
+
+            data.sortArray = sortArray;
+            this.$emit('sort', data);
+        },
+        sort(data) {
+            this.$emit('sort', data);
+        },
+        handleKeyEvent(e) {
+            switch (e.keyCode) {
+                case 13: // enter
+                    e.preventDefault();
+                    if (this.keyboardSelected) {
+                        this.storeKeyboardSorting();
+                    } else {
+                        this.keyboardSelected = true;
+                        const assistiveLive = 
+                            this.$gettextInterpolate(
+                                this.$gettext('%{elementTitle} ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen. Mit Pfeiltasten links und rechts kann die Position in der Hierarchie verändert werden.'),
+                                { elementTitle: this.element.attributes.title, pos: this.element.attributes.position + 1, listLength: this.siblingCount }
+                            );
+
+                            this.setAssistiveLiveContents(assistiveLive);
+                    }
+                    break;
+            }
+            if (this.keyboardSelected) {
+                const data = {
+                    element: this.element,
+                    parents: []
+                };
+                switch (e.keyCode) {
+                    case 27: // esc
+                    case 9: //tab
+                        this.abortKeyboardSorting();
+                        break;
+                    case 37: // left
+                        e.preventDefault();
+                        this.$emit('moveItemPrevLevel', data);
+                        break;
+                    case 38: // up
+                        e.preventDefault();    
+                        this.$emit('moveItemUp', data);
+                        break;
+                    case 39: // right
+                        e.preventDefault();
+                        this.$emit('moveItemNextLevel', data);
+                        break;
+                    case 40: // down
+                        e.preventDefault(); 
+                        this.$emit('moveItemDown', data);
+                        break;
+                }
+            }
+        },
+        moveItemPrevLevel(data) {
+            data.parents.push(this.element.id);
+            this.$emit('moveItemPrevLevel', data);
+        },
+        moveItemUp(data) {
+            data.parents.push(this.element.id);
+            this.$emit('moveItemUp', data);
+        },
+        moveItemNextLevel(data) {
+            data.parents.push(this.element.id);
+            this.$emit('moveItemNextLevel', data);
+        },
+        moveItemDown(data) {
+            data.parents.push(this.element.id);
+            this.$emit('moveItemDown', data);
+        },
+        abortKeyboardSorting() {
+            this.$emit('childrenUpdated');
+            const assistiveLive = this.$gettextInterpolate(
+                this.$gettext('%{elementTitle}. Neuordnung abgebrochen.'),
+                { elementTitle: this.element.attributes.title }
+            );
+            this.setAssistiveLiveContents(assistiveLive);
+            this.$nextTick(() => {
+                this.keyboardSelected = false;
+            });
+        },
+        storeKeyboardSorting() {
+            const data = {
+                id: this.element.id,
+                newPos: this.element.newPos,
+                oldPos: this.element.attributes.position,
+                oldParent: this.element.relationships.parent.data.id,
+                newParent: this.element.newParentId,
+                sortArray: this.element.sortArray
+            };
+            this.keyboardSelected = false;
+
+            if (data.newParent === undefined || data.newPos === undefined) {
+                const assistiveLive = this.$gettextInterpolate(
+                    this.$gettext('%{elementTitle}. Neuordnung nicht möglich.'),
+                    {elementTitle: this.element.attributes.title}
+                );
+                this.setAssistiveLiveContents(assistiveLive);
+                return;
+            }
+
+            if (data.oldParent === data.newParent && data.oldPos === data.newPos) {
+                const assistiveLive = this.$gettextInterpolate(
+                    this.$gettext('%{elementTitle}. Neuordnung abgebrochen.'),
+                    {elementTitle: this.element.attributes.title}
+                );
+                this.setAssistiveLiveContents(assistiveLive);
+                return;
+            }
+            this.$emit('sort', data);
+            const assistiveLive = this.$gettextInterpolate(
+                this.$gettext('%{elementTitle}, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.'), 
+                {elementTitle: this.element.attributes.title, pos: data.newPos + 1, listLength: this.siblingCount }
+            );
+            this.setAssistiveLiveContents(assistiveLive);
+        }
+    },
+    mounted() {
+        if (this.element.relationships?.task?.data) {
+            this.loadTask({
+                taskId: this.element.relationships.task.data.id,
+            });
+        }
+        if (this.newPos || this.newParentId) {
+            this.keyboardSelected = true;
+            this.$refs.sortableHandle.focus();
+        }
+    },
+    watch: {
+        newPos() {
+            this.keyboardSelected = true;
+            this.$refs.sortableHandle.focus();
+        },
+        newParentId() {
+            this.keyboardSelected = true;
+            this.$refs.sortableHandle.focus();
+        },
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareWelcomeScreen.vue b/resources/vue/components/courseware/structural-element/CoursewareWelcomeScreen.vue
new file mode 100644
index 0000000..5081368
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/CoursewareWelcomeScreen.vue
@@ -0,0 +1,88 @@
+<template>
+    <div class="cw-welcome-screen">
+        <div class="cw-welcome-screen-keyvisual"></div>
+        <header>
+            {{ $gettext('Willkommen bei Courseware') }}
+        </header>
+        <div class="cw-welcome-screen-actions">
+            <a href="https://hilfe.studip.de/help/5.0/de/Basis.Courseware" target="_blank" class="button">
+                {{ $gettext('Mehr über Courseware erfahren') }}
+            </a>
+            <button class="button" :title="$gettext('Fügt einen Standard-Abschnitt mit einem Text-Block hinzu')" @click="addDefault">
+                {{ $gettext('Ersten Inhalt erstellen') }}
+            </button>
+            <button class="button" @click="addContainer">
+                {{ $gettext('Einen Abschnitt auswählen') }}
+            </button>
+        </div>
+    </div>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-welcome-screen',
+    computed: {
+        ...mapGetters({
+            consumeMode: 'consumeMode',
+            lastCreatedBlocks: 'courseware-blocks/lastCreated',
+            lastCreatedContainers: 'courseware-containers/lastCreated'
+        }),
+    },
+    methods: {
+        ...mapActions({
+            createContainer: 'createContainer',
+            createBlock: 'createBlockInContainer',
+            coursewareBlockAdder: 'coursewareBlockAdder',
+            companionSuccess: 'companionSuccess',
+            updateContainer: 'updateContainer',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+
+            coursewareConsumeMode: 'coursewareConsumeMode',
+            coursewareViewMode: 'coursewareViewMode',
+            coursewareContainerAdder: 'coursewareContainerAdder',
+            coursewareShowToolbar: 'coursewareShowToolbar'
+
+        }),
+        addContainer() {
+            this.coursewareConsumeMode(false);
+            this.coursewareShowToolbar(true);
+            this.coursewareViewMode('edit');
+            this.$nextTick(() => {
+                this.coursewareContainerAdder(true);
+            });
+        },
+        async addDefault() {
+            let attributes = {};
+            attributes["container-type"] = 'list';
+            attributes.payload = {
+                colspan: 'full',
+                sections: [{ name: 'Liste', icon: '', blocks: [] }],
+            };
+            await this.createContainer({ structuralElementId: this.$route.params.id, attributes: attributes });
+            let newContainer = this.lastCreatedContainers;
+            await this.lockObject({ id: newContainer.id, type: 'courseware-containers' });
+            await this.createBlock({
+                container: newContainer,
+                section: 0,
+                blockType: 'text',
+            });
+            this.coursewareViewMode('edit');
+            this.coursewareConsumeMode(false);
+            this.companionSuccess({
+                info: this.$gettext('Das Elemente für Ihren ersten Inhalt wurde angelegt.'),
+            });
+            const newBlock = this.lastCreatedBlocks;
+            newContainer.attributes.payload.sections[0].blocks.push(newBlock.id);
+            const structuralElementId = this.$route.params.id
+            await this.updateContainer({ container: newContainer, structuralElementId: structuralElementId });
+            await this.unlockObject({ id: newContainer.id, type: 'courseware-containers' });
+            
+
+        }
+    }
+
+}
+</script>
diff --git a/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue
new file mode 100644
index 0000000..5651171
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue
@@ -0,0 +1,236 @@
+<template>
+    <focus-trap v-model="consumModeTrap">
+        <div
+            :class="{ 'cw-structural-element-consumemode': consumeMode }"
+            class="cw-structural-element"
+        >
+            <div v-if="structuralElement" class="cw-structural-element-content">
+                <courseware-ribbon :canEdit="false" :disableSettings="true" :disableAdder="true">
+                    <template #buttons>
+                        <router-link v-if="prevElement" :to="'/structural_element/' + prevElement.id">
+                            <div class="cw-ribbon-button cw-ribbon-button-prev" :title="textRibbon.perv" />
+                        </router-link>
+                        <div v-else class="cw-ribbon-button cw-ribbon-button-prev-disabled" :title="$gettext('keine vorherige Seite')"/>
+                        <router-link v-if="nextElement" :to="'/structural_element/' + nextElement.id">
+                            <div class="cw-ribbon-button cw-ribbon-button-next" :title="textRibbon.next" />
+                        </router-link>
+                        <div v-else class="cw-ribbon-button cw-ribbon-button-next-disabled" :title="$gettext('keine nächste Seite')"/>
+                    </template>
+                    <template #breadcrumbList>
+                        <li
+                            v-for="ancestor in ancestors"
+                            :key="ancestor.id"
+                            :title="ancestor.attributes.title"
+                            class="cw-ribbon-breadcrumb-item"
+                        >
+                            <span>
+                                <router-link :to="'/structural_element/' + ancestor.id">{{ ancestor.attributes.title || "–" }}</router-link>
+                            </span>
+                        </li>
+                        <li
+                            class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current"
+                            :title="structuralElement.attributes.title"
+                        >
+                            <span>{{ structuralElement.attributes.title || "–" }}</span>
+                        </li>
+                    </template>
+                    <template #breadcrumbFallback>
+                        <li
+                            class="cw-ribbon-breadcrumb-item cw-ribbon-breadcrumb-item-current"
+                            :title="structuralElement.attributes.title"
+                        >
+                            <span>{{ structuralElement.attributes.title }}</span>
+                        </li>
+                    </template>
+                </courseware-ribbon>
+
+                <div
+                    class="cw-container-wrapper"
+                    :class="{
+                        'cw-container-wrapper-consume': consumeMode,
+                    }"
+                >
+                    <div v-if="structuralElementLoaded" class="cw-companion-box-wrapper">
+                        <courseware-empty-element-box
+                            v-if="noContainers"
+                            :noContainers="noContainers"
+                        />
+                    </div>
+                    <component
+                        v-for="container in containers"
+                        :key="container.id"
+                        :is="containerComponent(container)"
+                        :container="container"
+                        :canEdit="false"
+                        :canAddElements="false"
+                        :isTeacher="false"
+                        class="cw-container-item"
+                    />
+                </div>
+            </div>
+        </div>
+    </focus-trap>
+</template>
+
+<script>
+import ContainerComponents from '../containers/container-components.js';
+import StructuralElementComponents from './structural-element-components.js';
+import CoursewarePluginComponents from '../plugin-components.js';
+import CoursewareCompanionOverlay from '../layouts/CoursewareCompanionOverlay.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'public-courseware-structural-element',
+
+    components: Object.assign(StructuralElementComponents, {
+        CoursewareCompanionOverlay,
+    }),
+
+    props: ['orderedStructuralElements', 'structuralElement'],
+
+    data() {
+        return {
+            consumModeTrap: false,
+            textRibbon: {
+                perv: this.$gettext('zurück'),
+                next: this.$gettext('weiter'),
+            },
+        }
+    },
+
+    computed: {
+        ...mapGetters({
+            courseware: 'courseware',
+            context: 'context',
+            consumeMode: 'consumeMode',
+            containerById: 'courseware-containers/byId',
+            pluginManager: 'pluginManager',
+            relatedContainers: 'courseware-containers/related',
+            relatedStructuralElements: 'courseware-structural-elements/related',
+            structuralElementById: 'courseware-structural-elements/byId',
+        }),
+
+        currentId() {
+            return this.structuralElement?.id;
+        },
+
+        image() {
+            return this.structuralElement.relationships?.image?.meta?.['download-url'] ?? null;
+        },
+
+        structuralElementLoaded() {
+            return this.structuralElement !== null && this.structuralElement !== {};
+        },
+
+        ancestors() {
+            if (!this.structuralElement) {
+                return [];
+            }
+
+            const finder = (parent) => {
+                const parentId = parent.relationships?.parent?.data?.id;
+                if (!parentId) {
+                    return null;
+                }
+                const element = this.structuralElementById({ id: parentId });
+
+                return element;
+            };
+
+            const visitAncestors = function* (node) {
+                const parent = finder(node);
+                if (parent) {
+                    yield parent;
+                    yield* visitAncestors(parent);
+                }
+            };
+
+            return [...visitAncestors(this.structuralElement)].reverse();
+        },
+
+        prevElement() {
+            const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id);
+            if (currentIndex <= 0) {
+                return null;
+            }
+            const previousId = this.orderedStructuralElements[currentIndex - 1];
+            const previous = this.structuralElementById({ id: previousId });
+
+            return previous;
+        },
+
+        nextElement() {
+            const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id);
+            const lastIndex = this.orderedStructuralElements.length - 1;
+            if (currentIndex === -1 || currentIndex === lastIndex) {
+                return null;
+            }
+            const nextId = this.orderedStructuralElements[currentIndex + 1];
+            const next = this.structuralElementById({ id: nextId });
+
+            return next;
+        },
+
+        empty() {
+            if (this.containers === null) {
+                return true;
+            } else {
+                return !this.containers.some((container) => container.relationships.blocks.data.length > 0);
+            }
+        },
+
+        containers() {
+            let containers = [];
+            let relatedContainers = this.structuralElement?.relationships?.containers?.data;
+
+            if (relatedContainers) {
+                for (const container of relatedContainers) {
+                    containers.push(this.containerById({ id: container.id}));
+                }
+            }
+
+            return containers;
+        },
+
+        noContainers() {
+            if (this.containers === null) {
+                return true;
+            } else {
+                return this.containers.length === 0;
+            }
+        },
+
+        isRoot() {
+            return this.structuralElement.id === this.context.rootId;
+        },
+    },
+
+    methods: {
+        ...mapActions({
+            companionError: 'companionError',
+            companionInfo: 'companionInfo',
+            companionSuccess: 'companionSuccess',
+        }),
+        containerComponent(container) {
+            return 'courseware-' + container.attributes['container-type'] + '-container';
+        },
+    },
+
+    created() {
+        this.pluginManager.registerComponentsLocally(this);
+    },
+
+    watch: {
+        consumeMode(newState) {
+            this.consumModeTrap = newState;
+        },
+    },
+
+    // this line provides all the components to courseware plugins
+    provide: () => ({
+        containerComponents: ContainerComponents,
+        coursewarePluginComponents: CoursewarePluginComponents,
+    }),
+};
+</script>
diff --git a/resources/vue/components/courseware/structural-element/structural-element-components.js b/resources/vue/components/courseware/structural-element/structural-element-components.js
new file mode 100644
index 0000000..fdba4b4
--- /dev/null
+++ b/resources/vue/components/courseware/structural-element/structural-element-components.js
@@ -0,0 +1,31 @@
+// contentbar
+import CoursewareRibbon from './CoursewareRibbon.vue';
+import CoursewareTabs from '../layouts/CoursewareTabs.vue';
+import CoursewareTab from '../layouts/CoursewareTab.vue';
+import { FocusTrap } from 'focus-trap-vue';
+//layout
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareEmptyElementBox from './CoursewareEmptyElementBox.vue';
+import IsoDate from '../layouts/IsoDate.vue';
+// containers
+import CoursewareAccordionContainer from '../containers/CoursewareAccordionContainer.vue';
+import CoursewareListContainer from '../containers/CoursewareListContainer.vue';
+import CoursewareTabsContainer from '../containers/CoursewareTabsContainer.vue';
+
+const StructuralElementComponents = {
+    //contentbar
+    CoursewareRibbon,
+    CoursewareTabs,
+    CoursewareTab,
+    FocusTrap,
+    //layout
+    CoursewareCompanionBox,
+    CoursewareEmptyElementBox,
+    IsoDate,
+    // containers
+    CoursewareAccordionContainer,
+    CoursewareListContainer,
+    CoursewareTabsContainer
+}
+
+export default StructuralElementComponents;
\ No newline at end of file
diff --git a/resources/vue/components/courseware/unit/CoursewareProgressCircle.vue b/resources/vue/components/courseware/unit/CoursewareProgressCircle.vue
new file mode 100644
index 0000000..e145590
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareProgressCircle.vue
@@ -0,0 +1,18 @@
+<template>
+    <div class="cw-progress-circle" :class="['p' + value, value > 50 ? 'over50' : '']">
+        <span>{{ value }} %</span>
+        <div class="left-half-clipper">
+            <div class="first50-bar"></div>
+            <div class="value-bar"></div>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'courseware-progress-circle',
+    props: {
+        value: Number,
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareSharedItems.vue b/resources/vue/components/courseware/unit/CoursewareSharedItems.vue
new file mode 100644
index 0000000..e68179c
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareSharedItems.vue
@@ -0,0 +1,88 @@
+<template>
+    <div class="cw-shared-items">
+        <h2 v-if="sharedElements.length > 0">{{ $gettext('Geteilte Lernmaterialien') }}</h2>
+        <ul class="cw-tiles">
+            <li
+                v-for="element in sharedElements"
+                :key="element.id"
+                class="tile"
+                :class="[element.attributes.payload.color, sharedElements.length > 3 ? '':  'cw-tile-margin']"
+            >
+                <a :href="getSharedElementUrl(element.id)" :title="element.attributes.title">
+                    <div
+                        class="preview-image"
+                        :class="[hasImage(element) ? '' : 'default-image']"
+                        :style="getChildStyle(element)"
+                    >
+                        <div class="overlay-text">{{ getOwnerName(element) }}</div>
+                    </div>
+                    <div class="description">
+                        <header
+                            :class="[element.attributes.purpose !== '' ? 'description-icon-' + element.attributes.purpose : '']"
+                        >
+                            {{ element.attributes.title }}
+                        </header>
+                        <div class="description-text-wrapper">
+                            <p>{{ element.attributes.payload.description }}</p>
+                        </div>
+                        <footer>
+                            {{ countChildren(element) + 1 }}
+                            <translate
+                                :translate-n="countChildren(element) + 1"
+                                translate-plural="Seiten"
+                            >
+                                Seite
+                            </translate>
+                        </footer>
+                    </div>
+                </a>
+            </li>
+        </ul>
+    </div>
+</template>
+<script>
+import { mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-shared-items',
+    computed: {
+        ...mapGetters({
+            sharedElements: 'courseware-structural-elements-shared/all',
+            userById: 'users/byId',
+        }),
+    },
+    methods: {
+        getChildStyle(child) {
+            let url = child.relationships?.image?.meta?.['download-url'];
+
+            if(url) {
+                return {'background-image': 'url(' + url + ')'};
+            } else {
+                return {};
+            }
+        },
+        hasImage(child) {
+            return child.relationships?.image?.data !== null;
+        },
+        getElementUrl(elementId) {
+            return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/courseware#/structural_element/' + elementId;
+        },
+        getSharedElementUrl(elementId) {
+            return STUDIP.URLHelper.base_url + 'dispatch.php/contents/courseware/shared_content_courseware/' + elementId;
+        },
+        getOwnerName(element) {
+            const ownerId = element.relationships.owner.data.id;
+            const owner = this.userById({ id: ownerId });
+
+            return owner.attributes['formatted-name']; 
+        },
+        countChildren(element) {
+            let data = element.relationships.children.data;
+            if (data) {
+                return data.length;
+            }
+            return 0;
+        },
+    },
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
new file mode 100644
index 0000000..6546552
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
@@ -0,0 +1,317 @@
+<template>
+    <studip-wizard-dialog
+        :title="$gettext('Lernmaterial hinzufügen')"
+        :confirmText="$gettext('Erstellen')"
+        :closeText="$gettext('Abbrechen')"
+        :slots="wizardSlots"
+        :lastRequiredSlotId="1"
+        :requirements="requirements"
+        @close="setShowUnitAddDialog(false)"
+        @confirm="createUnit"
+    >
+        <template v-slot:basic>
+            <form class="default" @submit.prevent="">
+                <label>
+                    <span>{{ text.title }}</span><span aria-hidden="true" class="wizard-required">*</span>
+                    <input type="text" v-model="addWizardData.title" required/>
+                </label>
+                <label>
+                    <span>{{ text.description }}</span><span aria-hidden="true" class="wizard-required">*</span>
+                    <textarea v-model="addWizardData.description" required/>
+                </label>
+            </form>
+        </template>
+        <template v-slot:layout>
+            <form class="default" @submit.prevent="">
+                <label>
+                        {{ $gettext('Bild hochladen') }}
+                        <br>
+                        <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile"/>
+                        <CoursewareCompanionBox
+                            v-if="uploadFileError"
+                            :msgCompanion="uploadFileError"
+                            mood="sad"
+                            class="cw-companion-box-in-form"
+                        />
+                </label>
+                <template v-if="selectedStockImage">
+                    <StockImageSelectableImageCard :stock-image="selectedStockImage" />
+                    <label>
+                        <button class="button" type="button" @click="selectedStockImage = null">
+                            {{ $gettext('Bild entfernen') }}
+                        </button>
+                    </label>
+                </template>
+                <label v-else>
+                    {{ $gettext('oder') }}
+                    <br>
+                    <button class="button" type="button" @click="showStockImageSelector = true">
+                        {{ $gettext('Aus dem Bilderpool auswählen') }}
+                    </button>
+                    <StockImageSelector v-if="showStockImageSelector" @close="showStockImageSelector = false" @select="onSelectStockImage" />
+                </label>
+                <label>
+                    {{ $gettext('Farbe') }}
+                    <studip-select
+                        v-model="addWizardData.color"
+                        :options="colors"
+                        :reduce="(color) => color.class"
+                        label="class"
+                    >
+                        <template #open-indicator="selectAttributes">
+                            <span v-bind="selectAttributes"
+                                ><studip-icon shape="arr_1down" :size="10"
+                            /></span>
+                        </template>
+                        <template #no-options>
+                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                        </template>
+                        <template #selected-option="{ name, hex }">
+                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                            ><span>{{ name }}</span>
+                        </template>
+                        <template #option="{ name, hex }">
+                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                            ><span>{{ name }}</span>
+                        </template>
+                    </studip-select>
+                </label>
+            </form>
+        </template>
+        <template v-slot:advanced>
+            <form class="default" @submit.prevent="">
+                <label>
+                    {{ $gettext('Art des Lernmaterials') }}
+                    <select v-model="addWizardData.purpose">
+                        <option value="content">{{ $gettext('Inhalt') }}</option>
+                        <option value="oer">{{ $gettext('OER-Material') }}</option>
+                        <option value="portfolio">{{ $gettext('ePortfolio') }}</option>
+                        <option value="draft">{{ $gettext('Entwurf') }}</option>
+                        <option v-if="!inCourseContext" value="template">{{ $gettext('Aufgabenvorlage') }}</option>
+                        <option value="other">{{ $gettext('Sonstiges') }}</option>
+                    </select>
+                </label>
+                <label>
+                    {{ $gettext('Lizenztyp') }}
+                    <select v-model="addWizardData.license_type">
+                        <option v-for="license in licenses" :key="license.id" :value="license.id">
+                            {{ license.name }}
+                        </option>
+                    </select>
+                </label>
+                <label>
+                    {{ $gettext('Geschätzter zeitlicher Aufwand') }}
+                    <input type="text" v-model="addWizardData.required_time" />
+                </label>
+                <label>
+                    {{ $gettext('Niveau') }}<br />
+                    {{ $gettext('von') }}
+                    <select v-model="addWizardData.difficulty_start">
+                        <option
+                            v-for="difficulty_start in 12"
+                            :key="difficulty_start"
+                            :value="difficulty_start"
+                        >
+                            {{ difficulty_start }}
+                        </option>
+                    </select>
+                    {{ $gettext('bis') }}
+                    <select v-model="addWizardData.difficulty_end">
+                        <option
+                            v-for="difficulty_end in 12"
+                            :key="difficulty_end"
+                            :value="difficulty_end"
+                        >
+                            {{ difficulty_end }}
+                        </option>
+                    </select>
+                </label>
+            </form>
+        </template>
+    </studip-wizard-dialog>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import StockImageSelectableImageCard from '../../stock-images/SelectableImageCard.vue';
+import StockImageSelector from '../../stock-images/SelectorDialog.vue';
+import StudipSelect from '../../StudipSelect.vue';
+import StudipWizardDialog from '../../StudipWizardDialog.vue';
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-shelf-dialog-add',
+    mixins: [colorMixin],
+    components: {
+        CoursewareCompanionBox,
+        StockImageSelectableImageCard,
+        StockImageSelector,
+        StudipWizardDialog,
+        StudipSelect,
+    },
+    data() {
+        return {
+            wizardSlots: [
+                { id: 1, valid: false, name: 'basic', title: this.$gettext('Grundeinstellungen'), icon: 'courseware',
+                  description: this.$gettext('Wählen Sie einen kurzen, prägnanten Titel und beschreiben Sie in einigen Worten den Inhalt des Lernmaterials. Eine Beschreibung erleichtert Lernenden die Auswahl des Lernmaterials.') },
+                { id: 2, valid: true, name: 'layout', title: this.$gettext('Erscheinung'), icon: 'picture',
+                  description: this.$gettext('Ein Vorschaubild motiviert Lernende das Lernmaterial zu erkunden. Die Kombination aus Bild und Farbe erleichtert das wiederfinden des Lernmaterials in der Übersicht.') },
+                { id: 3, valid: true, name: 'advanced', title: this.$gettext('Zusatzangaben'), icon: 'info-list',
+                  description: this.$gettext('Hier können Sie detaillierte Angaben zum Lernmaterial eintragen. Diese sind besonders interessant wenn das Lernmaterial als OER geteilt wird.') }
+            ],
+            text: {
+                title: this.$gettext('Titel des Lernmaterials'),
+                description: this.$gettext('Beschreibung')
+            },
+            addWizardData: {},
+            uploadFileError: '',
+            requirements: [],
+            showStockImageSelector: false,
+            selectedStockImage: null,
+        }
+    },
+    computed: {
+        ...mapGetters({
+            licenses: 'licenses',
+            context: 'context',
+            lastCreateCoursewareUnit: 'courseware-units/lastCreated',
+            structuralElementById:  'courseware-structural-elements/byId',
+        }),
+        inCourseContext() {
+            return this.context.type === 'courses';
+        },
+        colors() {
+            return this.mixinColors.filter(color => color.darkmode);
+        }
+    },
+    mounted() {
+        this.initAddWizardData();
+    },
+    methods: {
+        ...mapActions({
+            companionError: 'companionError',
+            companionInfo: 'companionInfo',
+            companionSuccess: 'companionSuccess',
+            createCoursewareUnit: 'courseware-units/create',
+            setShowUnitAddDialog: 'setShowUnitAddDialog',
+            setStockImageForStructuralElement: 'setStockImageForStructuralElement',
+            loadStructuralElementById: 'courseware-structural-elements/loadById',
+            uploadImageForStructuralElement: 'uploadImageForStructuralElement',
+        }),
+        initAddWizardData() {
+            this.addWizardData = {
+                title: '',
+                description: '',
+                purpose: 'content',
+                color: 'studip-blue',
+            }
+        },
+        validateSlots() {
+            let valid = true;
+            this.wizardSlots.forEach(slot => {
+                if (!slot.valid) {
+                    valid = false;
+                }
+            });
+
+            return valid;
+        },
+        checkUploadFile() {
+            const file = this.$refs?.upload_image?.files[0];
+            if (file.size > 2097152) {
+                this.uploadFileError = this.$gettext('Diese Datei ist zu groß. Bitte wählen Sie eine Datei aus, die kleiner als 2MB ist.');
+            } else if (!file.type.includes('image')) {
+                this.uploadFileError = this.$gettext('Diese Datei ist kein Bild. Bitte wählen Sie ein Bild aus.');
+            } else {
+                this.uploadFileError = '';
+                this.selectedStockImage = null;
+            }
+        },
+        async createUnit() {
+            if (!this.validateSlots()) {
+                this.companionError({
+                    info: this.$gettext('Bitte füllen Sie alle notwendigen Angaben aus.'),
+                });
+                return false;
+            }
+            const file = this.$refs?.upload_image?.files[0];
+            const unit = {
+                attributes: {
+                    title: this.addWizardData.title,
+                    purpose: this.addWizardData.purpose,
+                    payload: {
+                        description: this.addWizardData.description,
+                        color: this.addWizardData.color,
+                        license_type: this.addWizardData.license_type,
+                        required_time: this.addWizardData.required_time,
+                        difficulty_start: this.addWizardData.difficulty_start,
+                        difficulty_end: this.addWizardData.difficulty_end
+                    }
+                },
+                relationships: {
+                    range: {
+                        data: {
+                            type: this.context.type,
+                            id: this.context.id
+                        }
+                    }
+                }
+            };
+            this.setShowUnitAddDialog(false);
+
+            await this.createCoursewareUnit(unit, { root: true });
+            this.companionSuccess({ info: this.$gettext('Neues Lernmaterial angelegt.') });
+            const newElementId = this.lastCreateCoursewareUnit.relationships['structural-element'].data.id
+            await this.loadStructuralElementById({ id: newElementId });
+            let newStructuralElement = this.structuralElementById({id: newElementId});
+
+            try {
+                if (file) {
+                    await this.uploadImageForStructuralElement({
+                        structuralElement: newStructuralElement,
+                        file,
+                    });
+                } else if (this.selectedStockImage) {
+                    await this.setStockImageForStructuralElement({
+                        structuralElement: newStructuralElement,
+                        stockImage: this.selectedStockImage,
+                    })
+                }
+
+                this.loadStructuralElementById({id: newStructuralElement.id, options: {include: 'children'}});
+            } catch(error) {
+                console.error(error);
+                this.companionError({ info: this.$gettext('Das Bild für das neue Lernmaterial konnte nicht gespeichert werden.') });
+            }
+        },
+        onSelectStockImage(stockImage) {
+            if (this.$refs?.upload_image) {
+                this.$refs.upload_image.value = null;
+            }
+            this.selectedStockImage = stockImage;
+            this.showStockImageSelector = false;
+        },
+    },
+    watch: {
+        addWizardData: {
+            handler(newData) {
+                this.requirements = [];
+                const slot = this.wizardSlots[0];
+                if (newData.title !== '' && newData.description !== '') {
+                    slot.valid = true;
+                }
+                if (newData.title === '' ) {
+                    slot.valid = false;
+                    this.requirements.push({slot: slot, text: this.text.title });
+                }
+                if (newData.description === '') {
+                    slot.valid = false;
+                    this.requirements.push({slot:  slot, text: this.text.description });
+                }
+            },
+            deep: true
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
new file mode 100644
index 0000000..d68ffa5
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
@@ -0,0 +1,401 @@
+<template>
+    <studip-wizard-dialog
+        :title="$gettext('Lernmaterial kopieren')"
+        :confirmText="$gettext('Kopieren')"
+        :closeText="$gettext('Abbrechen')"
+        :lastRequiredSlotId="2"
+        :requirements="requirements"
+        :slots="wizardSlots"
+        @close="close"
+        @confirm="copy"
+    >
+        <template v-slot:source>
+            <form class="default" @submit.prevent="">
+                <fieldset class="radiobutton-set">
+                    <template v-if="inCourseContext">
+                        <input
+                            id="cw-shelf-copy-source-self"
+                            type="radio"
+                            v-model="source"
+                            value="self"
+                            :aria-description="text.sourceSelf"
+                        />
+                        <label @click="source = 'self'" for="cw-shelf-copy-source-self">
+                            <div class="icon"><studip-icon shape="seminar" size="32"/></div>
+                            <div class="text">{{ text.sourceSelf }}</div>
+                            <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
+                            <studip-icon shape="check-circle" size="24" class="check" />
+                        </label>
+                    </template>
+                    <input
+                        id="cw-shelf-copy-source-courses"
+                        type="radio"
+                        v-model="source"
+                        value="courses"
+                        :aria-description="text.sourceCourses"
+                    />
+                    <label @click="source = 'courses'" for="cw-shelf-copy-source-courses">
+                        <div class="icon"><studip-icon shape="seminar" size="32"/></div>
+                        <div class="text">{{ text.sourceCourses }}</div>
+                        <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
+                        <studip-icon shape="check-circle" size="24" class="check" />
+                    </label>
+                    <input
+                        id="cw-shelf-copy-source-users"
+                        type="radio"
+                        v-model="source"
+                        value="users"
+                        :aria-description="text.sourceUsers"
+                    />
+                    <label @click="source = 'users'" for="cw-shelf-copy-source-users">
+                        <div class="icon"><studip-icon shape="content" size="32"/></div>
+                        <div class="text">{{ text.sourceUsers }}</div>
+                        <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
+                        <studip-icon shape="check-circle" size="24" class="check" />
+                    </label>
+                </fieldset>
+                <template v-if="source === 'courses'">
+                    <label>
+                        <span>{{ $gettext('Semester') }}</span><span aria-hidden="true"></span>
+                        <select v-model="selectedSemester">
+                            <option value="all">{{ $gettext('Alle Semester') }}</option>
+                            <option v-for="semester in semesterMap" :key="semester.id" :value="semester.id">
+                                {{ semester.attributes.title }}
+                            </option>
+                        </select>
+                    </label>
+                    <label>
+                        <span>{{ $gettext('Veranstaltung') }}</span><span aria-hidden="true" class="wizard-required">*</span>
+                        <studip-select
+                            v-if="filteredCourses.length !== 0 && !loadingCourses"
+                            :options="filteredCourses"
+                            :clearable="false"
+                            :reduce="option => option.id"
+                            :getOptionLabel="option => option.attributes.title"
+                            v-model="selectedRange"
+                        >
+                            <template #open-indicator="selectAttributes">
+                                <span v-bind="selectAttributes"
+                                    ><studip-icon shape="arr_1down" size="10"
+                                /></span>
+                            </template>
+                        </studip-select>
+                        <p v-if="loadingCourses">
+                            {{$gettext('Lade Veranstaltungen…')}}
+                        </p>
+                        <p v-if="filteredCourses.length === 0 && !loadingCourses">
+                            {{$gettext('Es wurden keine geeigneten Veranstaltungen gefunden.')}}
+                        </p>
+                    </label>
+                </template>
+
+            </form>
+        </template>
+        <template v-slot:unit>
+            <form class="default" @submit.prevent="">
+                <fieldset v-if="units.length !== 0" class="radiobutton-set">
+                    <template v-for="unit in units">
+                        <input
+                            :id="'cw-shelf-copy-unit-' + unit.id"
+                            type="radio"
+                            v-model="selectedUnit"
+                            :checked="unit.id === selectedUnitId"
+                            :value="unit"
+                            :key="'radio-' + unit.id"
+                            :aria-description="unit.element.attributes.title"
+                        />
+                        <label @click="selectedUnit = unit" :key="'label-' + unit.id" :for="'cw-shelf-copy-unit-' + unit.id">
+                            <div class="icon"><studip-icon shape="courseware" size="32"/></div>
+                            <div class="text">{{ unit.element.attributes.title }}</div>
+                            <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
+                            <studip-icon shape="check-circle" size="24" class="check" />
+                        </label>
+                    </template>
+                </fieldset>
+                <courseware-companion-box
+                    v-else
+                    mood="sad"
+                    :msgCompanion="$gettext('Für die gewählte Quelle stehen keine Lernmaterialien zur Verfügung.')"
+                />
+            </form>
+        </template>
+        <template v-slot:edit>
+            <form v-if="selectedUnit" class="default" @submit.prevent="">
+                <label>
+                    <span>{{$gettext('Titel')}}</span><span aria-hidden="true" class="wizard-required">*</span>
+                    <input type="text" v-model="modifiedTitle" :placeholder="selectedUnitTitle" required />
+                </label>
+                <label>
+                    {{$gettext('Farbe')}}
+                    <studip-select
+                        v-model="modifiedColor"
+                        :options="colors"
+                        :reduce="(color) => color.class"
+                        :clearable="false"
+                        label="class"
+                    >
+                        <template #open-indicator="selectAttributes">
+                            <span v-bind="selectAttributes"
+                                ><studip-icon shape="arr_1down" size="10"
+                            /></span>
+                        </template>
+                        <template #no-options>
+                            {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+                        </template>
+                        <template #selected-option="{ name, hex }">
+                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                            ><span>{{ name }}</span>
+                        </template>
+                        <template #option="{ name, hex }">
+                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                            ><span>{{ name }}</span>
+                        </template>
+                    </studip-select>
+                </label>
+                <label>
+                    <span>{{$gettext('Beschreibung')}}</span><span aria-hidden="true" class="wizard-required">*</span>
+                    <textarea v-model="modifiedDescription" :placeholder="selectedUnitDescription" required />
+                </label>
+            </form>
+            <courseware-companion-box
+                    v-else
+                    mood="pointing"
+                    :msgCompanion="$gettext('Bitte wählen Sie ein Lernmaterial aus.')"
+                />
+        </template>
+    </studip-wizard-dialog>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import StudipSelect from '../../StudipSelect.vue';
+import StudipWizardDialog from '../../StudipWizardDialog.vue';
+
+import { mapActions, mapGetters } from 'vuex'
+
+export default {
+    name: 'courseware-shelf-dialog-copy',
+    mixins: [colorMixin],
+    components: {
+        CoursewareCompanionBox,
+        StudipWizardDialog,
+        StudipSelect,
+    },
+    data() {
+        return {
+            wizardSlots: [
+                { id: 1, valid: false, name: 'source', title: this.$gettext('Quelle'), icon: 'source',
+                  description: this.$gettext('Wählen Sie hier den Ort in Stud.IP aus, an dem sich das zu kopierende Lernmaterial befindet.') },
+                { id: 2, valid: false, name: 'unit', title: this.$gettext('Lernmaterial'), icon: 'courseware',
+                  description: this.$gettext('Wählen Sie hier das gewünschte Lernmaterial aus der Liste aus. Eine Auswahl wird durch einen grauen Hintergrund und einen Kontrollhaken angezeigt.') },
+                { id: 3, valid: true, name: 'edit', title: this.$gettext('Anpassen'), icon: 'edit',
+                  description: this.$gettext('Sie können hier die Daten des zu kopierenden Lernmaterials anpassen. Eine Anpassung ist optional, Sie können das Lernmaterial auch unverändert kopieren.') },
+            ],
+            source: '',
+            loadingCourses: false,
+            courses: [],
+            semesterMap: [],
+            selectedSemester: 'all',
+            selectedRange: '',
+            loadingUnits: false,
+            selectedUnit: null,
+            selectedUnitElement: null,
+            modifiedTitle: '',
+            modifiedColor: '',
+            modifiedDescription: '',
+
+            requirements: [],
+            text: {
+                source: this.$gettext('Quelle'),
+                unit: this.$gettext('Lernmaterial'),
+                sourceSelf: this.$gettext('Diese Veranstaltung'),
+                sourceCourses: this.$gettext('Veranstaltung'),
+                sourceUsers: this.$gettext('Arbeitsplatz'),
+
+            }
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId',
+            coursewareUnits: 'courseware-units/all',
+            semesterById: 'semesters/byId',
+            structuralElementById: 'courseware-structural-elements/byId',
+            context: 'context'
+        }),
+        colors() {
+            return this.mixinColors.filter(color => color.darkmode);
+        },
+        units() {
+            let units = this.coursewareUnits.filter(unit => unit.relationships.range.data.id === this.selectedRange);
+            units.forEach(unit => {
+                unit.element = this.getUnitElement(unit);
+            });
+
+            if (this.inCourseContext) {
+                units = units.filter(unit => unit.element.attributes.purpose !== 'template');
+            }
+
+            return units;
+        },
+        selectedUnitId() {
+            return this.selectedUnit?.id;
+        },
+        inCourseContext() {
+            return this.context.type === 'courses';
+        },
+        selectedUnitTitle() {
+            return this.selectedUnitElement.attributes.title ?? '';
+        },
+        selectedUnitDescription() {
+            return this.selectedUnitElement.attributes.payload.description ?? '';
+        },
+        filteredCourses() {
+            const courses = this.courses.filter((course) => { return course.id !== this.context.id });
+            if (this.selectedSemester === 'all') {
+                return courses;
+            } else {
+                return courses.filter((course) => {
+                    return course.relationships['start-semester'].data.id === this.selectedSemester;
+                });
+            }
+        }
+    },
+    async mounted() {
+        this.initWizardData();
+    },
+    methods: {
+        ...mapActions({
+            companionSuccess: 'companionSuccess',
+            loadCourseUnits: 'loadCourseUnits',
+            loadUsersCourses: 'loadUsersCourses',
+            loadSemester: 'semesters/loadById',
+            loadUserUnits: 'loadUserUnits',
+            setShowUnitCopyDialog: 'setShowUnitCopyDialog',
+            copyUnit: 'copyUnit',
+        }),
+        initWizardData() {
+            this.source = this.inCourseContext ? 'self' : 'users';
+            this.selectedRange = '';
+            this.selectedUnit = null;
+        },
+        close() {
+            this.setShowUnitCopyDialog(false);
+            this.initWizardData();
+        },
+        getUnitElement(unit) {
+            return this.structuralElementById({id: unit.relationships['structural-element'].data.id});
+        },
+        async copy() {
+            if (this.selectedUnit) {
+                const element = this.getUnitElement(this.selectedUnit);
+                const modified = {
+                        title: this.modifiedTitle !== '' ? this.modifiedTitle : this.selectedUnitTitle,
+                        color: this.modifiedColor,
+                        description: this.modifiedDescription !== '' ? this.modifiedDescription : this.selectedUnitDescription
+                }
+                await this.copyUnit({ unitId: this.selectedUnit.id, modified: modified });
+                this.companionSuccess({ info: this.$gettext('Lernmaterial kopiert.') });
+                this.close();
+            }
+        },
+        async updateCourses() {
+            this.loadingCourses = true;
+            this.courses = await this.loadUsersCourses({ userId: this.userId, withCourseware: true });
+            this.loadSemesterMap();
+            this.loadingCourses = false;
+        },
+        loadSemesterMap() {
+            let view = this;
+            let semesters = [];
+            this.courses.every(course => {
+                let semId = course.relationships['start-semester'].data.id;
+                if(!semesters.includes(semId)) {
+                    semesters.push(semId);
+                }
+                return true;
+            });
+            semesters.every(semester => {
+                view.loadSemester({id: semester}).then( () => {
+                    view.semesterMap.push(view.semesterById({id: semester}));
+                    view.semesterMap.sort((a, b) => new Date(b.attributes.start) - new Date(a.attributes.start));
+                });
+                return true;
+            });
+        },
+        async updateCourseUnits(cid) {
+            this.loadingUnits = true;
+            await this.loadCourseUnits(cid);
+            this.loadingUnits = false;
+        },
+        setElementData() {
+            this.selectedUnitElement = this.getUnitElement(this.selectedUnit);
+            this.modifiedTitle = this.selectedUnitElement.attributes.title;
+            this.modifiedColor = this.selectedUnitElement.attributes.payload.color;
+            this.modifiedDescription = this.selectedUnitElement.attributes.payload.description;
+        },
+        resetElementData() {
+            this.modifiedTitle = '';
+            this.modifiedColor = '';
+            this.modifiedDescription = '';
+        },
+        validateSelection() {
+            this.requirements = [];
+            if (this.selectedRange === '') {
+                this.requirements.push({slot: this.wizardSlots[0], text: this.text.source });
+            }
+            if (this.selectedUnit === null) {
+                this.requirements.push({slot: this.wizardSlots[1], text: this.text.unit });
+            }
+        }
+    },
+    watch: {
+        selectedUnit(newUnit) {
+            this.validateSelection();
+            const slot = this.wizardSlots[1];
+            if (newUnit !== null) {
+                slot.valid = true;
+                this.setElementData();
+            } else {
+                slot.valid = false;
+                this.resetElementData();
+            }
+        },
+        selectedRange(newRid) {
+            this.selectedUnit = null;
+            this.validateSelection();
+            const slot = this.wizardSlots[0];
+            
+            if (newRid !== '') {
+                slot.valid = true;
+                if (this.source === 'courses' || this.source === 'self') {
+                    this.updateCourseUnits(newRid);
+                }
+                if (this.source === 'users') {
+                    this.loadUserUnits(newRid);
+                }
+            } else {
+                slot.valid = false;
+            }
+        },
+        source(newSource) {
+            switch (newSource) {
+                case 'self':
+                    this.selectedRange = this.context.id;
+                    break;
+                case 'courses':
+                    this.selectedRange = '';
+                    this.updateCourses();
+                    break;
+                case 'users':
+                    this.selectedRange = this.userId;
+                    break;
+            }
+        },
+        selectedSemester(newSemester) {
+            this.selectedRange = '';
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogImport.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogImport.vue
new file mode 100644
index 0000000..8717aab
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogImport.vue
@@ -0,0 +1,373 @@
+<template>
+    <div class="cw-shelf-dialog-import-wrapper">
+        <studip-wizard-dialog
+            v-if="!importRunning"
+            :title="$gettext('Lernmaterial importieren')"
+            :confirmText="$gettext('Importieren')"
+            :closeText="$gettext('Abbrechen')"
+            :slots="wizardSlots"
+            :lastRequiredSlotId="1"
+            :requirements="requirements"
+            @close="setShowUnitImportDialog(false)"
+            @confirm="importCoursewareArchiv"
+        >
+            <template v-slot:file>
+                <form class="default" @submit.prevent="">
+                    <label>
+                        <span>{{ text.import }}</span><span aria-hidden="true" class="wizard-required">*</span>
+                        <input v-show="importZipFile === null" ref="fileInput" class="cw-file-input" type="file" accept=".zip" @change="setImport" />
+                        <p v-show="importZipFile !== null" class="cw-file-input-change">
+                            <button class="button" @click="$refs.fileInput.click()">{{ $gettext('Datei ändern')}}</button><span>{{ importZipFile?.name }}</span>
+                        </p>
+                    </label>
+                    <fieldset v-show="archiveErrors.length > 0">
+                        <legend>{{$gettext('Fehler im Import-Archiv')}}</legend>
+                        <ul>
+                            <li v-for="(error, index) in archiveErrors" :key="index"> {{error}} </li>
+                        </ul>
+                    </fieldset>
+                </form>
+            </template>
+            <template v-slot:edit>
+                <form v-if="hasValidFile" class="default" @submit.prevent="">
+                    <label>
+                        {{ text.title }}
+                        <input type="text" v-model="modifiedData.title" :placeholder="loadedTitle" required />
+                    </label>
+                    <label>
+                        {{ text.color }}
+                        <studip-select
+                            v-model="modifiedData.color"
+                            :options="colors"
+                            :reduce="(color) => color.class"
+                            :clearable="false"
+                            label="class"
+                        >
+                            <template #open-indicator="selectAttributes">
+                                <span v-bind="selectAttributes"
+                                    ><studip-icon shape="arr_1down" size="10"
+                                /></span>
+                            </template>
+                            <template #no-options>
+                                {{$gettext('Es steht keine Auswahl zur Verfügung.')}}
+                            </template>
+                            <template #selected-option="{ name, hex }">
+                                <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                                ><span>{{ name }}</span>
+                            </template>
+                            <template #option="{ name, hex }">
+                                <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                                ><span>{{ name }}</span>
+                            </template>
+                        </studip-select>
+                    </label>
+                    <label>
+                        {{ text.description }}
+                        <textarea v-model="modifiedData.description" :placeholder="loadedDescription" required />
+                    </label>
+                </form>
+                <courseware-companion-box 
+                    v-else
+                    :msgCompanion="$gettext('Bitte wählen Sie ein Import-Archiv aus.')"
+                    mood="unsure"
+                />
+            </template>
+        </studip-wizard-dialog>
+        <studip-dialog
+            v-if="importRunning"
+            :title="$gettext('Lernmaterial importieren')"
+            :closeText="$gettext('Schließen')"
+            height="420"
+            @close="setShowUnitImportDialog(false)"
+        >
+            <template v-slot:dialogContent>
+                <div role="status" aria-live="polite">
+                    <courseware-companion-box 
+                        v-show="importDone && importErrors.length === 0"
+                        :msgCompanion="$gettext('Import erfolgreich!')"
+                        mood="special"
+                    />
+                    <courseware-companion-box
+                        v-show="importDone && importErrors.length > 0"
+                        :msgCompanion="$gettext('Import abgeschlossen. Es sind Fehler aufgetreten!')"
+                        mood="unsure"
+                    />
+                    <courseware-companion-box
+                        v-show="!importDone"
+                        :msgCompanion="$gettext('Import läuft. Bitte schließen Sie den Dialog nicht bis der Import abgeschlossen wurde.')"
+                        mood="pointing"
+                    />
+                </div>
+                <form v-if="!importDone" class="default" @submit.prevent="">
+                    <fieldset>
+                        <div v-if="!fileImportDone" class="cw-import-zip">
+                            <header>{{$gettext('Importiere Dateien')}}:</header>
+                            <div class="progress-bar-wrapper">
+                                <div class="progress-bar" role="progressbar" :style="{width: importFilesProgress + '%'}" :aria-valuenow="importFilesProgress" aria-valuemin="0" aria-valuemax="100">{{ importFilesProgress }}%</div>
+                            </div>
+                            {{ importFilesState }}
+                        </div>
+                        <div v-if="fileImportDone" class="cw-import-zip">
+                            <header>{{$gettext('Importiere Elemente')}}:</header>
+                            <div class="progress-bar-wrapper">
+                                <div class="progress-bar" role="progressbar" :style="{width: importStructuresProgress + '%'}" :aria-valuenow="importStructuresProgress" aria-valuemin="0" aria-valuemax="100">{{ importStructuresProgress }}%</div>
+                            </div>
+                            {{ importStructuresState }}
+                        </div>
+                    </fieldset>
+                </form>
+                <form v-if="importErrors.length > 0" class="default" @submit.prevent="">
+                    <fieldset>
+                        <legend>{{$gettext('Fehlermeldungen')}}</legend>
+                        <ul>
+                            <li v-for="(error, index) in importErrors" :key="index"> {{error}} </li>
+                        </ul>
+                    </fieldset>
+                </form>
+            </template>
+        </studip-dialog>
+    </div>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareImport from '@/vue/mixins/courseware/import.js';
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import StudipWizardDialog from '../../StudipWizardDialog.vue';
+
+import { mapActions, mapGetters } from 'vuex'
+import JSZip from 'jszip';
+
+export default {
+    name: 'courseware-shelf-dialog-import',
+    components: {
+        StudipWizardDialog,
+        CoursewareCompanionBox
+    },
+    mixins: [CoursewareImport, colorMixin],
+    data() {
+        return {
+            wizardSlots: [
+                { id: 1, valid: false, name: 'file', title: this.$gettext('Import-Archiv'), icon: 'file-archive',
+                  description: this.$gettext('Wählen Sie hier eine Courseware-Export-Archiv-Datei von Ihrer Festplatte aus. Bei Courseware-Export-Archiven handelt es sich um Zip-Dateien. Diese sollten mindestens die Dateien files.json und courseware.json enthalten.') },
+                { id: 2, valid: true, name: 'edit', title: this.$gettext('Anpassen'), icon: 'edit', description: this.$gettext('Sie können hier die Daten des zu importierenden Lernmaterials anpassen. Eine Anpassung ist optional, Sie können das Archiv auch unverändert importieren.') },
+            ],
+            modifiedData: {
+                title: '',
+                color: 'studip-blue',
+                description: ''
+            },
+            importArchivFile: null,
+            importRunning: false,
+            importZipFile: null,
+            zip: null,
+
+            loadedZipData: null,
+            archiveErrors: [],
+
+            requirements: [],
+            text: {
+                import: this.$gettext('Importdatei'),
+                title: this.$gettext('Titel'),
+                color: this.$gettext('Farbe'),
+                description: this.$gettext('Beschreibung'),
+            }
+        }
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            importFilesState: 'importFilesState',
+            importFilesProgress: 'importFilesProgress',
+            importStructuresState: 'importStructuresState',
+            importStructuresProgress: 'importStructuresProgress',
+            importErrors: 'importErrors',
+            lastCreateCoursewareUnit: 'courseware-units/lastCreated',
+            
+        }),
+        colors() {
+            return this.mixinColors.filter(color => color.darkmode);
+        },
+        fileImportDone() {
+            return this.importFilesProgress === 100;
+        },
+        importDone() {
+            return this.importFilesProgress === 100 && this.importStructuresProgress === 100;
+        },
+        hasValidFile() {
+            return this.archiveErrors.length === 0 && this.loadedZipData !== null;
+        },
+        loadedTitle() {
+            return this.loadedZipData.courseware.attributes.title ?? '';
+        },
+        loadedDescription() {
+            return this.loadedZipData.courseware.attributes.payload.description ?? '';
+        }
+    },
+    methods: {
+        ...mapActions({
+            setShowUnitImportDialog: 'setShowUnitImportDialog',
+            createCoursewareUnit: 'courseware-units/create',
+            setImportFilesProgress: 'setImportFilesProgress',
+            setImportStructuresProgress: 'setImportStructuresProgress',
+            setImportErrors: 'setImportErrors',
+            loadStructuralElementById: 'courseware-structural-elements/loadById',
+            companionSuccess: 'companionSuccess',
+        }),
+        setImport(event) {
+            this.importZipFile = event.target.files[0];
+            this.loadZipData(); 
+        },
+
+        async loadZipData() {
+            const slot = this.wizardSlots[0];
+            const text = this.text.import;
+            this.archiveErrors = [];
+            this.loadedZipData = null;
+            this.modifiedData.title = '';
+            this.modifiedData.color = 'studip-blue';
+            this.modifiedData.description = '';
+            let filesError = false;
+            if (!this.importZipFile.type.includes('zip')) {
+                this.archiveErrors.push(this.$gettext('Die gewählte Datei ist kein Archiv.'));
+                filesError = true;
+            }
+            if (!filesError) {
+                try {
+                    this.zip = await JSZip.loadAsync(this.importZipFile);
+                } catch(error) {
+                    this.zip = null;
+                    this.archiveErrors.push(this.$gettext('Beim laden des Archivs ist ein Fehler aufgetreten. Vermutlich ist das Archiv beschädigt.'));
+                    filesError = true;
+                }
+                
+                if (this.zip) {
+                    if (this.zip.file('courseware.json') === null) {
+                        this.archiveErrors.push(this.$gettext('Das Archiv enthält keine courseware.json Datei.'));
+                        filesError = true;
+                    }
+                    if (this.zip.file('files.json') === null) {
+                        this.archiveErrors.push(this.$gettext('Das Archiv enthält keine files.json Datei.'));
+                        filesError = true;
+                    }
+                    if (this.zip.file('data.xml') !== null) {
+                        this.archiveErrors.push(this.$gettext(
+                            'Das Archiv enthält eine data.xml Datei. Möglicherweise handelt es sich um einen Export aus dem Courseware-Plugin. Diese Archive sind nicht kompatibel mit dieser Courseware.'
+                        ));
+                        filesError = true;
+                    }
+                }
+            }
+            if (filesError) {
+                this.updateRequirements(slot, text, false);
+                slot.valid = false;
+                return;
+            } else {
+                this.updateRequirements(slot, text, true);
+                slot.valid = true;
+            }
+
+            let data = await this.zip.file('courseware.json').async('string');
+            let courseware = null;
+            let data_settings = null;
+            let settings = null;
+            let data_files = await this.zip.file('files.json').async('string');
+            let files = null;
+            let jsonErrors = false;
+
+            try {
+                courseware = JSON.parse(data);
+            } catch (error) {
+                jsonErrors = true;
+                this.archiveErrors.push(this.$gettext('Die Beschreibung der Courseware-Inhalte ist nicht valide.'));
+                this.archiveErrors.push(error);
+            }
+
+            if (this.zip.file('settings.json') !== null) {
+                data_settings = await this.zip.file('settings.json').async('string');
+                try {
+                    settings = JSON.parse(data_settings);
+                } catch (error) {
+                    jsonErrors = true;
+                    this.archiveErrors.push(this.$gettext('Die Beschreibung der Courseware-Einstellungen ist nicht valide.'));
+                    this.archiveErrors.push(error);
+                }
+            }
+
+            try {
+                files = JSON.parse(data_files);
+            } catch (error) {
+                jsonErrors = true;
+                this.archiveErrors.push(this.$gettext('Die Beschreibung der Dateien ist nicht valide.'));
+                this.archiveErrors.push(error);
+            }
+            if (jsonErrors) {
+                return;
+            }
+
+            this.loadedZipData = {
+                courseware: courseware,
+                files: files,
+                settings: settings
+            }
+
+            this.modifiedData.title = courseware.attributes.title;
+            this.modifiedData.color = courseware.attributes.payload.color ?? 'studip-blue';
+            this.modifiedData.description = courseware.attributes.payload.description ?? '';
+        },
+
+        async importCoursewareArchiv() {
+            if (this.loadedZipData === null) {
+                return false;
+            }
+
+            this.setImportFilesProgress(0);
+            this.setImportStructuresProgress(0);
+            this.setImportErrors([]);
+
+            this.importRunning = true;
+
+            const title = this.modifiedData.title !==  '' ? this.modifiedData.title : this.loadedTitle;
+            const description = this.modifiedData.description !==  '' ? this.modifiedData.description : this.loadedDescription;
+
+            const unit = {
+                    attributes: {
+                        title: title,
+                        payload: {
+                            description: description,
+                            color: this.modifiedData.color,
+                        }
+                    },
+                    relationships: {
+                        range: {
+                            data: {
+                                type: this.context.type,
+                                id: this.context.id
+                            }
+                        }
+                    }
+                };
+            await this.createCoursewareUnit(unit, { root: true });
+            const newElementId = this.lastCreateCoursewareUnit.relationships['structural-element'].data.id;
+            await this.loadStructuralElementById({ id: newElementId });
+
+            const newStructuralElement = this.structuralElementById({id: newElementId});
+
+            await this.importCourseware(this.loadedZipData.courseware, newStructuralElement.id, this.loadedZipData.files, 'migrate', this.loadedZipData.settings);
+            this.companionSuccess({ info: this.$gettext('Lernmaterial importiert.') });
+        },
+        updateRequirements(slot, text, valid) {
+            const index = this.requirements.findIndex(req =>  req.slot.id === slot.id && req.text === text);
+            if (valid) {
+                if (index !== -1) {
+                    this.requirements.splice(index, 1);
+                }
+            } else {
+                if (index === -1) {
+                   this.requirements.push({slot: slot, text: text});
+                }
+            }
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
new file mode 100644
index 0000000..74366d3
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
@@ -0,0 +1,197 @@
+<template>
+    <li class="courseware-unit-item">
+        <courseware-tile
+            tag="div"
+            :color="color"
+            :title="title"
+            :descriptionLink="url"
+            :descriptionTitle="$gettext('Lernmaterial öffnen')"
+            :displayProgress="inCourseContext"
+            :progress="progress"
+            :imageUrl="imageUrl"
+        >
+            <template #image-overlay-with-action-menu>
+                <studip-action-menu
+                    class="cw-unit-action-menu"
+                    :items="menuItems"  
+                    :context="title"
+                    :collapseAt="0"
+                    @showDelete="openDeleteDialog"
+                    @showExport="openExportDialog"
+                    @showProgress="openProgressDialog"
+                    @showSettings="openSettingsDialog"
+                    @showLayout="openLayoutDialog"
+                    @copyUnit="copy"
+                />
+            </template>
+            <template #description>
+                {{ description }}
+            </template>
+        </courseware-tile>
+        <studip-dialog
+            v-if="showDeleteDialog"
+            :title="$gettext('Lernmaterial löschen')"
+            :question="$gettextInterpolate(
+                        $gettext('Möchten Sie das Lernmaterial %{ unitTitle } wirklich löschen?'),
+                        { unitTitle: title }
+                    )"
+            height="200"
+            @confirm="executeDelete"
+            @close="closeDeleteDialog"
+        ></studip-dialog>
+
+        <studip-dialog
+            v-if="showProgressDialog"
+            :title="userIsTeacher ? $gettext('Fortschritt aller Teilnehmenden') : $gettext('Mein Fortschritt')"
+            :closeText="$gettext('Schließen')"
+            closeClass="cancel"
+            width="800"
+            height="600"
+            @close="closeProgressDialog"
+        >
+            <template v-slot:dialogContent>
+                <courseware-unit-progress :progressData="progresses" :unitId="unit.id" :rootId="parseInt(unitElement.id)"/>
+            </template>
+        </studip-dialog>
+
+        <courseware-unit-item-dialog-export v-if="showExportDialog" :unit="unit" @close="showExportDialog = false" />
+        <courseware-unit-item-dialog-settings v-if="showSettingsDialog" :unit="unit" @close="closeSettingsDialog"/>
+        <courseware-unit-item-dialog-layout v-if="showLayoutDialog" :unitElement="unitElement" @close="closeLayoutDialog"/>
+    </li>
+</template>
+
+<script>
+import CoursewareTile from '../layouts/CoursewareTile.vue';
+import CoursewareUnitItemDialogExport from './CoursewareUnitItemDialogExport.vue';
+import CoursewareUnitItemDialogSettings from './CoursewareUnitItemDialogSettings.vue';
+import CoursewareUnitItemDialogLayout from './CoursewareUnitItemDialogLayout.vue';
+import CoursewareUnitProgress from './CoursewareUnitProgress.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-unit-item',
+    components: {
+        CoursewareTile,
+        CoursewareUnitItemDialogExport,
+        CoursewareUnitItemDialogLayout,
+        CoursewareUnitItemDialogSettings,
+        CoursewareUnitProgress,
+    },
+    props: {
+        unit: Object,
+    },
+    data() {
+        return {
+            showDeleteDialog: false,
+            showExportDialog: false,
+            showSettingsDialog: false,
+            showProgressDialog: false,
+            showLayoutDialog: false,
+            progresses: null
+        }
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            structuralElementById: 'courseware-structural-elements/byId',
+            userIsTeacher: 'userIsTeacher'
+        }),
+        menuItems() {
+            let menu = [];
+            if (this.inCourseContext) {
+                menu.push({ id: 1, label: this.$gettext('Fortschritt'), icon: 'progress', emit: 'showProgress' });
+            }
+            if(this.userIsTeacher && this.inCourseContext) {
+                menu.push({ id: 2, label: this.$gettext('Einstellungen'), icon: 'settings', emit: 'showSettings' });
+            }
+            if(this.userIsTeacher || !this.inCourseContext) {
+                menu.push({ id: 4, label: this.$gettext('Darstellung'), icon: 'colorpicker', emit: 'showLayout' });
+                menu.push({ id: 4, label: this.$gettext('Duplizieren'), icon: 'copy', emit: 'copyUnit' });
+                menu.push({ id: 5, label: this.$gettext('Exportieren'), icon: 'export', emit: 'showExport' });
+                menu.push({ id: 6, label: this.$gettext('Löschen'), icon: 'trash', emit: 'showDelete' });
+            }
+
+            return menu;
+        },
+        unitElement() {
+            return this.structuralElementById({id: this.unit.relationships['structural-element'].data.id}) ?? null;
+        },
+        color() {
+            return this.unitElement?.attributes?.payload?.color ?? 'studip-blue';
+        },
+        title() {
+            return  this.unitElement?.attributes?.title ?? '';
+        },
+        description() {
+            return  this.unitElement?.attributes?.payload?.description ?? '';
+        },
+        imageUrl() {
+            return this.unitElement?.relationships?.image?.meta?.['download-url'] ?? '';
+        },
+        url() {
+            if (this.inCourseContext) {
+                return STUDIP.URLHelper.getURL('dispatch.php/course/courseware/courseware/' + this.unit.id , { cid: this.context.id });
+            } else {
+                return STUDIP.URLHelper.getURL('dispatch.php/contents/courseware/courseware/' + this.unit.id);
+            }
+        },
+        progress() {
+            if (this.unitElement) {
+                return this.progresses?.[this.unitElement.id]?.progress?.cumulative ?? 0;
+            }
+            return 0;
+        },
+        inCourseContext() {
+            return this.context.type === 'courses';
+        }
+    },
+    async mounted() {
+        if (this.inCourseContext) {
+            this.progresses = await this.loadUnitProgresses({unitId: this.unit.id});
+        }
+    },
+    methods: {
+        ...mapActions({
+            deleteUnit: 'deleteUnit',
+            loadUnitProgresses: 'loadUnitProgresses',
+            copyUnit: 'copyUnit',
+            companionSuccess: 'companionSuccess'
+        }),
+        executeDelete() {
+            this.deleteUnit({id: this.unit.id});
+        },
+        openDeleteDialog() {
+            this.showDeleteDialog = true;
+        },
+        closeDeleteDialog() {
+            this.showDeleteDialog = false;
+        },
+        openExportDialog() {
+            this.showExportDialog = true;
+        },
+        async openProgressDialog() {
+            this.showProgressDialog = true;
+            this.progresses = await this.loadUnitProgresses({unitId: this.unit.id});
+        },
+        closeProgressDialog() {
+            this.showProgressDialog = false;
+        },
+        openSettingsDialog() {
+            this.showSettingsDialog = true; 
+        },
+        closeSettingsDialog() {
+            this.showSettingsDialog = false;
+        },
+        openLayoutDialog() {
+            this.showLayoutDialog = true; 
+        },
+        closeLayoutDialog() {
+            this.showLayoutDialog = false;
+        },
+        async copy() {
+            await this.copyUnit({unitId: this.unit.id, modified: null});
+            this.companionSuccess({ info: this.$gettext('Lernmaterial kopiert.') });        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogExport.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogExport.vue
new file mode 100644
index 0000000..08f0a21
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogExport.vue
@@ -0,0 +1,132 @@
+<template>
+        <studip-dialog
+            :title="$gettext('Lernmaterial exportieren')"
+            :confirmText="$gettext('Exportieren')"
+            confirmClass="accept"
+            :closeText="$gettext('Schließen')"
+            closeClass="cancel"
+            height="350"
+            @close="$emit('close')"
+            @confirm="executeExport"
+        >
+            <template v-slot:dialogContent>
+                <courseware-companion-box
+                    v-show="!exportRunning"
+                    :msgCompanion="$gettextInterpolate($gettext('Export des Lernmaterials: %{title}'), {title: title})"
+                    mood="curious"
+                />
+
+                <courseware-companion-box
+                    v-show="exportRunning"
+                    :msgCompanion="$gettextInterpolate($gettext('%{title} wird exportiert, bitte haben sie einen Moment Geduld...'), {title: title})"
+                    mood="pointing"
+                />
+                <div v-show="exportRunning" class="cw-import-zip">
+                    <header>{{ exportState }}:</header>
+                    <div class="progress-bar-wrapper">
+                        <div
+                            class="progress-bar"
+                            role="progressbar"
+                            :style="{ width: exportProgress + '%' }"
+                            :aria-valuenow="exportProgress"
+                            aria-valuemin="0"
+                            aria-valuemax="100"
+                        >
+                            {{ exportProgress }}%
+                        </div>
+                    </div>
+                </div>
+            </template>
+        </studip-dialog>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareExport from '@/vue/mixins/courseware/export.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-unit-item-dialog-export',
+    mixins: [CoursewareExport],
+    components: {
+        CoursewareCompanionBox,
+    },
+    props: {
+        unit: Object
+    },
+    data() {
+        return {
+            currentInstance: null,
+            exportRunning: false,
+        }
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            exportProgress: 'exportProgress',
+            exportState: 'exportState',
+            instanceById: 'courseware-instances/byId',
+            structuralElementById: 'courseware-structural-elements/byId',
+            userIsTeacher: 'userIsTeacher', 
+        }),
+        instance() {
+            if (this.inCourseContext) {
+                return this.instanceById({id: 'course_' + this.context.id + '_' + this.unit.id});
+            } else {
+                return this.instanceById({id: 'user_' + this.context.id + '_' + this.unit.id});
+            }
+            
+        },
+        inCourseContext() {
+            return this.context.type === 'courses';
+        },
+        unitElement() {
+            return this.structuralElementById({id: this.unit.relationships['structural-element'].data.id}) ?? null;
+        },
+        title() {
+            return  this.unitElement?.attributes?.title ?? '';
+        },
+    },
+    methods: {
+        ...mapActions({
+            loadInstance: 'loadInstance',
+            setExportState: 'setExportState',
+            companionSuccess: 'companionSuccess'
+        }),
+        async loadUnitInstance() {
+            const context = {type: this.context.type, id: this.context.id, unit: this.unit.id};
+            await this.loadInstance(context);
+        },
+        async executeExport() {
+            if (this.exportRunning) {
+                return;
+            }
+
+            this.exportRunning = true;
+            
+            this.setExportState(this.$gettext('Lade Einstellungen'));
+            await this.loadUnitInstance();
+            this.setExportState('');
+
+            await this.sendExportZip(this.unitElement.id, {
+                withChildren: true,
+                completeExport: true,
+                settings: {
+                    'editing-permission-level': this.instance.attributes['editing-permission-level'] ?? 'tutor',
+                    'sequential-progression': this.instance.attributes['sequential-progression'] ?? 0,
+                    'certificate-settings': this.instance.attributes['certificate-settings'],
+                    'reminder-settings': this.instance.attributes['reminder-settings'],
+                    'reset-progress-settings': this.instance.attributes['reset-progress-settings']
+                }
+            });
+
+            this.exportRunning = false;
+            this.$emit('close');
+        },
+
+    },
+    async mounted() {
+        await this.loadUnitInstance();
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
new file mode 100644
index 0000000..f06f7fd
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
@@ -0,0 +1,197 @@
+<template>
+        <studip-dialog
+        :title="$gettext('Darstellung')"
+        :confirmText="$gettext('Speichern')"
+        confirmClass="accept"
+        :closeText="$gettext('Schließen')"
+        closeClass="cancel"
+        height="720"
+        width="500"
+        @close="$emit('close')"
+        @confirm="storeLayout"
+    >
+        <template v-slot:dialogContent>
+            <form v-if="currentElement" class="default" @submit.prevent="">
+                <label>{{ $gettext('Vorschaubild') }}</label>
+                <img
+                    v-if="showPreviewImage"
+                    :src="image"
+                    class="cw-structural-element-image-preview"
+                    :alt="$gettext('Vorschaubild')"
+                />
+                <label v-if="showPreviewImage">
+                    <button class="button" @click="deleteImage">{{ $gettext('Bild löschen') }}</button>
+                </label>
+                <courseware-companion-box
+                    v-if="uploadFileError"
+                    :msgCompanion="uploadFileError"
+                    mood="sad"
+                />
+                <label v-if="!showPreviewImage">
+                    <img
+                        v-if="currentFile"
+                        :src="uploadImageURL"
+                        class="cw-structural-element-image-preview"
+                        :alt="$gettext('Vorschaubild')"
+                    />
+                    <div v-else class="cw-structural-element-image-preview-placeholder"></div>
+                    {{ $gettext('Bild hochladen') }}
+                    <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
+                </label>
+
+                <label>
+                    {{ $gettext('Titel') }}
+                    <input type="text" v-model="currentElement.attributes.title"/>
+                </label>
+                <label>
+                    {{ $gettext('Beschreibung') }}
+                    <textarea
+                        v-model="currentElement.attributes.payload.description"
+                        class="cw-structural-element-description"
+                    />
+                </label>
+                <label>
+                    {{ $gettext('Farbe') }}
+                    <studip-select
+                        v-model="currentElement.attributes.payload.color"
+                        :options="colors"
+                        :reduce="(color) => color.class"
+                        label="class"
+                        class="cw-vs-select"
+                    >
+                        <template #open-indicator="selectAttributes">
+                            <span v-bind="selectAttributes"
+                                ><studip-icon shape="arr_1down" :size="10"
+                            /></span>
+                        </template>
+                        <template #no-options>
+                            {{ $gettext('Es steht keine Auswahl zur Verfügung') }}.
+                        </template>
+                        <template #selected-option="{ name, hex }">
+                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                            ><span>{{ name }}</span>
+                        </template>
+                        <template #option="{ name, hex }">
+                            <span class="vs__option-color" :style="{ 'background-color': hex }"></span
+                            ><span>{{ name }}</span>
+                        </template>
+                    </studip-select>
+                </label>
+            </form>
+        </template>
+    </studip-dialog>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import { mapActions, mapGetters } from 'vuex';
+
+
+export default {
+    name: 'courseware-unit-item-dialog-layout',
+    components: {
+        CoursewareCompanionBox
+    },
+    props: {
+        unitElement: Object
+    },
+    mixins: [colorMixin],
+    data() {
+        return {
+            currentElement: null,
+            deletingPreviewImage: false,
+            uploadFileError: '',
+            currentFile: null,
+            uploadImageURL: null,
+        }
+    },
+    computed: {
+        ...mapGetters({
+            userId: 'userId'
+        }),
+        colors() {
+            return this.mixinColors.filter(color => color.darkmode);
+        },
+        image() {
+            return this.currentElement.relationships?.image?.meta?.['download-url'] ?? null;
+        },
+
+        showPreviewImage() {
+            return this.image !== null && this.deletingPreviewImage === false;
+        },
+    },
+    methods: {
+        ...mapActions({
+            companionSuccess: 'companionSuccess',
+            companionWarning: 'companionWarning',
+            loadStructuralElement: 'loadStructuralElement',
+            lockObject: 'lockObject',
+            unlockObject: 'unlockObject',
+            updateStructuralElement: 'updateStructuralElement',
+            uploadImageForStructuralElement: 'uploadImageForStructuralElement',
+            deleteImageForStructuralElement: 'deleteImageForStructuralElement',
+        }),
+        initData() {
+            this.currentElement = _.cloneDeep(this.unitElement);
+        },
+        checkUploadFile() {
+            const file = this.$refs?.upload_image?.files[0];
+            if (file.size > 2097152) {
+                this.uploadFileError = this.$gettext('Diese Datei ist zu groß. Bitte wählen Sie eine kleinere Datei.');
+            } else if (!file.type.includes('image')) {
+                this.uploadFileError = this.$gettext('Diese Datei ist kein Bild. Bitte wählen Sie ein Bild aus.');
+            } else {
+                this.uploadFileError = '';
+                this.currentFile = file;
+                this.uploadImageURL = window.URL.createObjectURL(file);
+            }
+        },
+        deleteImage() {
+            if (!this.deletingPreviewImage) {
+                this.deletingPreviewImage = true;
+            }
+        },
+        async storeLayout() {
+            this.$emit('close');
+            await this.loadStructuralElement(this.currentElement.id);
+            if (
+                this.unitElement.relationships['edit-blocker'].data !== null
+                && this.unitElement.relationships['edit-blocker'].data?.id !== this.userId
+            ) {
+                this.companionWarning({
+                    info: this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, die Daten werden bereits von einem anderen Nutzer bearbeitet.')
+                });
+                return false;
+            } else {
+                await this.lockObject({ id: this.currentElement.id, type: 'courseware-structural-elements' });
+            }
+
+            if (this.currentFile) {
+                this.uploadImageForStructuralElement({
+                    structuralElement: this.currentElement,
+                    file: this.currentFile,
+                }).catch((error) => {
+                    console.error(error);
+                    this.companionWarning({
+                        info: this.$gettext('Beim Hochladen der Bilddatei ist ein Fehler aufgetretten.')
+                    });
+                });
+                await this.loadStructuralElement(this.currentElement.id);
+            } else if (this.deletingPreviewImage) {
+                await this.deleteImageForStructuralElement(this.currentElement);
+            }
+
+            await this.updateStructuralElement({
+                element: this.currentElement,
+                id: this.currentElement.id,
+            });
+            await this.unlockObject({ id: this.currentElement.id, type: 'courseware-structural-elements' });
+        }
+    },
+    async mounted() {
+        this.initData();
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue
new file mode 100644
index 0000000..34718fd
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue
@@ -0,0 +1,311 @@
+<template>
+    <studip-dialog
+        :title="$gettext('Einstellungen')"
+        :confirmText="$gettext('Speichern')"
+        confirmClass="accept"
+        :closeText="$gettext('Schließen')"
+        closeClass="cancel"
+        height="600"
+        width="500"
+        @close="$emit('close')"
+        @confirm="storeSettings"
+    >
+        <template v-slot:dialogContent>
+            <form v-if="!loadSettings" class="default" @submit.prevent="">
+                <fieldset>
+                    <legend>{{ $gettext('Allgemeine Einstellungen') }}</legend>
+                    <label>
+                        <span>{{ $gettext('Art der Inhaltsabfolge') }}</span>
+                        <select class="size-s" v-model="currentProgression">
+                            <option value="0">{{ $gettext('Frei') }}</option>
+                            <option value="1">{{ $gettext('Sequentiell') }}</option>
+                        </select>
+                    </label>
+
+                    <label>
+                        <span>{{ $gettext('Editierberechtigung für Tutor/-innen') }}</span>
+                        <select class="size-s" v-model="currentPermissionLevel">
+                            <option value="dozent">{{ $gettext('Nein') }}</option>
+                            <option value="tutor">{{ $gettext('Ja') }}</option>
+                        </select>
+                    </label>
+                </fieldset>
+                <fieldset>
+                    <legend>{{ $gettext('Zertifikate') }}</legend>
+                    <label>
+                        <input type="checkbox" name="makecert" v-model="makeCert">
+                        <span>
+                            {{ $gettext('Zertifikat bei Erreichen einer Fortschrittsgrenze versenden') }}
+                        </span>
+                        <studip-tooltip-icon :text="$gettext('Erreicht eine Person in diesem Lernmaterial den ' +
+                            'hier eingestellten Fortschritt, so erhält Sie ein PDF-Zertifikat per E-Mail.')"/>
+                    </label>
+                    <label v-if="makeCert">
+                        <span>
+                            {{ $gettext('Erforderlicher Fortschritt (in Prozent), um ein Zertifikat zu erhalten') }}
+                        </span>
+                        <input type="number" min="1" max="100" name="threshold" v-model="certThreshold">
+                    </label>
+                    <label v-if="makeCert">
+                        <span>
+                            {{ $gettext('Hintergrundbild des Zertifikats wählen') }}
+                        </span>
+                        <courseware-file-chooser :isImage="true" v-model="certImage" @selectFile="updateCertImage" />
+                    </label>
+                </fieldset>
+                <fieldset>
+                    <legend>
+                        {{ $gettext('Erinnerungen') }}
+                    </legend>
+                    <label>
+                        <input type="checkbox" name="sendreminders" v-model="sendReminders">
+                        <span>
+                            {{ $gettext('Erinnerungsnachrichten an alle Teilnehmenden schicken') }}
+                        </span>
+                        <studip-tooltip-icon :text="$gettext('Hier können periodisch Nachrichten an alle ' +
+                        'Teilnehmenden verschickt werden, um z.B. an die Bearbeitung dieses Lernmaterials zu erinnern.')"/>
+                    </label>
+
+                    <label v-if="sendReminders">
+                        <span>
+                            {{ $gettext('Zeitraum zwischen Erinnerungen') }}
+                        </span>
+                        <select name="reminder_interval" v-model="reminderInterval">
+                            <option value="7">
+                                {{ $gettext('wöchentlich') }}
+                            </option>
+                            <option value="14">
+                                {{ $gettext('14-tägig') }}
+                            </option>
+                            <option value="30">
+                                {{ $gettext('monatlich') }}
+                            </option>
+                            <option value="90">
+                                {{ $gettext('vierteljährlich') }}
+                            </option>
+                            <option value="180">
+                                {{ $gettext('halbjährlich') }}
+                            </option>
+                            <option value="365">
+                                {{ $gettext('jährlich') }}
+                            </option>
+                        </select>
+                    </label>
+                    <label v-if="sendReminders" class="col-3">
+                        <span>
+                            {{ $gettext('Erstmalige Erinnerung am') }}
+                            <input type="date" name="reminder_start_date"
+                                v-model="reminderStartDate">
+                        </span>
+                    </label>
+                    <label v-if="sendReminders" class="col-3">
+                        <span>
+                            {{ $gettext('Letztmalige Erinnerung am') }}
+                            <input type="date" name="reminder_end_date"
+                                v-model="reminderEndDate">
+                        </span>
+                    </label>
+                    <label v-if="sendReminders">
+                        <span>
+                            {{ $gettext('Text der Erinnerungsmail') }}
+                            <textarea cols="70" rows="4" name="reminder_mail_text" data-editor="minimal"
+                                    v-model="reminderMailText"></textarea>
+                        </span>
+                    </label>
+                </fieldset>
+                <fieldset>
+                    <legend>
+                        {{ $gettext('Fortschritt') }}
+                    </legend>
+                    <label>
+                        <input type="checkbox" name="resetprogress" v-model="resetProgress">
+                        <span>
+                            {{ $gettext('Fortschritt periodisch auf 0 zurücksetzen') }}
+                        </span>
+                        <studip-tooltip-icon :text="$gettext('Hier kann eingestellt werden, den Fortschritt ' +
+                            'aller Teilnehmenden periodisch auf 0 zurückzusetzen.')"/>
+                    </label>
+                    <label v-if="resetProgress">
+                        <span>
+                            {{ $gettext('Zeitraum zum Rücksetzen des Fortschritts') }}
+                        </span>
+                        <select name="reset_progress_interval" v-model="resetProgressInterval">
+                            <option value="14">
+                                {{ $gettext('14-tägig') }}
+                            </option>
+                            <option value="30">
+                                {{ $gettext('monatlich') }}
+                            </option>
+                            <option value="90">
+                                {{ $gettext('vierteljährlich') }}
+                            </option>
+                            <option value="180">
+                                {{ $gettext('halbjährlich') }}
+                            </option>
+                            <option value="365">
+                                {{ $gettext('jährlich') }}
+                            </option>
+                        </select>
+                    </label>
+                    <label v-if="resetProgress" class="col-3">
+                        <span>
+                            {{ $gettext('Erstmaliges Zurücksetzen am') }}
+                            <input type="date" dataformatas="" name="reset_progress_start_date"
+                                v-model="resetProgressStartDate">
+                        </span>
+                    </label>
+                    <label v-if="resetProgress" class="col-3">
+                        <span>
+                            {{ $gettext('Letztmaliges Zurücksetzen am') }}
+                            <input type="date" name="reset_progress_end_date"
+                                v-model="resetProgressEndDate">
+                        </span>
+                    </label>
+                    <label v-if="resetProgress">
+                        <span>
+                            {{ $gettext('Text der Rücksetzungsmail') }}
+                            <textarea cols="70" rows="4" name="reset_progress_mail_text" data-editor="minimal"
+                                    v-model="resetProgressMailText"></textarea>
+                        </span>
+                    </label>
+                </fieldset>
+            </form>
+            <studip-progress-indicator v-else :description="$gettext('Lade Einstellungen…')"/>
+
+        </template>
+    </studip-dialog>
+</template>
+
+<script>
+import CoursewareFileChooser from '../layouts/CoursewareFileChooser.vue';
+import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-unit-item-dialog-settings',
+    components: {
+        CoursewareFileChooser,
+        StudipProgressIndicator,
+    },
+    props: {
+        unit: Object
+    },
+    data() {
+        return {
+            currentInstance: null,
+            loadSettings: false,
+            currentPermissionLevel: '',
+            currentProgression: 0,
+            makeCert: false,
+            certThreshold: 0,
+            certImage: '',
+            sendReminders: false,
+            reminderInterval: 7,
+            reminderStartDate: '',
+            reminderEndDate: '',
+            reminderMailText: '',
+            resetProgress: false,
+            resetProgressInterval: 180,
+            resetProgressStartDate: '',
+            resetProgressEndDate: '',
+            resetProgressMailText: ''
+        }
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            instanceById: 'courseware-instances/byId',
+            userIsTeacher: 'userIsTeacher'
+        }),
+        instance() {
+            if (this.inCourseContext) {
+                return this.instanceById({id: 'course_' + this.context.id + '_' + this.unit.id});
+            } else {
+                return this.instanceById({id: 'user_' + this.context.id + '_' + this.unit.id});
+            }
+            
+        },
+        inCourseContext() {
+            return this.context.type === 'courses';
+        }
+    },
+    methods: {
+        ...mapActions({
+            loadInstance: 'loadInstance',
+            storeCoursewareSettings: 'storeCoursewareSettings',
+            companionSuccess: 'companionSuccess'
+        }),
+        async loadUnitInstance() {
+            const context = {type: this.context.type, id: this.context.id, unit: this.unit.id};
+            await this.loadInstance(context);
+        },
+        initData() {
+            this.currentPermissionLevel = this.currentInstance.attributes['editing-permission-level'];
+            this.currentProgression = this.currentInstance.attributes['sequential-progression'] ? '1' : '0';
+            this.certSettings = this.currentInstance.attributes['certificate-settings'];
+            this.makeCert = typeof(this.certSettings) === 'object' &&
+                Object.keys(this.certSettings).length > 0;
+            this.certThreshold = this.certSettings.threshold;
+            this.certImage = this.certSettings.image;
+            this.reminderSettings = this.currentInstance.attributes['reminder-settings'];
+            this.sendReminders = typeof(this.reminderSettings) === 'object' &&
+                Object.keys(this.reminderSettings).length > 0;
+            this.reminderInterval = this.reminderSettings.interval;
+            this.reminderStartDate = this.reminderSettings.startDate;
+            this.reminderEndDate = this.reminderSettings.endDate;
+            this.reminderMailText = this.reminderSettings.mailText;
+            this.resetProgressSettings = this.currentInstance.attributes['reset-progress-settings'];
+            this.resetProgress = typeof(this.resetProgressSettings) === 'object' &&
+                Object.keys(this.resetProgressSettings).length > 0;
+            this.resetProgressInterval = this.resetProgressSettings.interval;
+            this.resetProgressStartDate = this.resetProgressSettings.startDate;
+            this.resetProgressEndDate = this.resetProgressSettings.endDate;
+            this.resetProgressMailText = this.resetProgressSettings.mailText;
+        },
+        storeSettings() {
+            this.$emit('close');
+            this.currentInstance.attributes['editing-permission-level'] = this.currentPermissionLevel;
+            this.currentInstance.attributes['sequential-progression'] = this.currentProgression;
+            this.currentInstance.attributes['certificate-settings'] = this.generateCertificateSettings();
+            this.currentInstance.attributes['reminder-settings'] = this.generateReminderSettings();
+            this.currentInstance.attributes['reset-progress-settings'] = this.generateResetProgressSettings();
+            this.storeCoursewareSettings({
+                instance: this.currentInstance,
+            });
+        },
+        generateCertificateSettings() {
+            return this.makeCert ? {
+                threshold: this.certThreshold,
+                image: this.certImage
+            } : {};
+        },
+        generateReminderSettings() {
+            return this.sendReminders ? {
+                interval: this.reminderInterval,
+                startDate: this.reminderStartDate,
+                endDate: this.reminderEndDate,
+                mailText: this.reminderMailText
+            } : {};
+        },
+        generateResetProgressSettings() {
+            return this.resetProgress ? {
+                interval: this.resetProgressInterval,
+                startDate: this.resetProgressStartDate,
+                endDate: this.resetProgressEndDate,
+                mailText: this.resetProgressMailText
+            } : {};
+        },
+        updateCertImage(file) {
+            this.certImage = file.id;
+        }
+    },
+    async mounted() {
+        this.loadSettings = true;
+        await this.loadUnitInstance();
+        this.loadSettings = false;
+        this.currentInstance = this.instance;
+        this.initData();
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItems.vue b/resources/vue/components/courseware/unit/CoursewareUnitItems.vue
new file mode 100644
index 0000000..9d07e26
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItems.vue
@@ -0,0 +1,80 @@
+<template>
+    <div class="cw-unit-items">
+        <h2 v-if="!inCourseContext && hasUnits">{{ $gettext('Persönliche Lernmaterialien') }}</h2>
+        <ul v-if="hasUnits" class="cw-tiles">
+            <courseware-unit-item v-for="unit in units" :key="unit.id" :unit="unit"/>
+        </ul>
+        <template v-if="!hasUnits && inCourseContext">
+            <div v-if="userIsTeacher" class="cw-contents-overview-teaser">
+                <div class="cw-contents-overview-teaser-content">
+                    <header>{{ $gettext('Lernmaterialien') }}</header>
+                    <p>
+                        {{ $gettext('Mit Courseware können Sie interaktive, multimediale Lerninhalte erstellen und nutzen. ' +
+                                    'Die Lerninhalte lassen sich hierarchisch unterteilen und können aus Texten, Videosequenzen, ' +
+                                    'Aufgaben, Kommunikationselementen und einer Vielzahl weiterer Elemente bestehen. ' +
+                                    'Fertige Lerninhalte können exportiert und in andere Kurse oder andere Installationen importiert werden. ' +
+                                    'Courseware ist nicht nur für digitale Formate geeignet, sondern kann auch genutzt werden, ' +
+                                    'um klassische Präsenzveranstaltungen mit Online-Anteilen zu ergänzen. Formate wie integriertes Lernen ' +
+                                    '(Blended Learning) lassen sich mit Courseware ideal umsetzen. Kollaboratives Lernen kann dank Schreibrechtevergabe ' +
+                                    'und dem Einsatz von Courseware in Studiengruppen realisiert werden.') }}
+                    </p>
+                    <button class="button" @click="setShowUnitAddDialog(true)">
+                        {{ $gettext('Neues Lernmaterial anlegen') }}
+                    </button>
+                </div>
+            </div>
+            <courseware-companion-box
+                v-else
+                :msgCompanion="$gettext('Es wurden leider noch keine Lernmaterialien angelegt.')"
+                mood="sad"
+            />
+        </template>
+        <div v-if="!hasUnits && !inCourseContext" class="cw-contents-overview-teaser">
+            <div class="cw-contents-overview-teaser-content">
+                <header>{{ $gettext('Ihre persönlichen Lernmaterialien') }}</header>
+                <p>{{ $gettext('Erstellen und verwalten Sie hier Ihre eigenen persönlichen Lernmaterialien in Form von ePorfolios, ' +
+                               'Vorlagen für Veranstaltungen oder einfach nur persönliche Inhalte für das Studium. ' +
+                               'Entwickeln Sie Ihre eigenen (Lehr-)Materialien für Studium oder die Lehre und teilen diese mit anderen Nutzenden.') }}</p>
+                <button class="button" @click="setShowUnitAddDialog(true)">
+                    {{ $gettext('Neues Lernmaterial anlegen') }}
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareUnitItem from './CoursewareUnitItem.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-unit-items',
+    components: {
+        CoursewareCompanionBox,
+        CoursewareUnitItem,
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            coursewareUnits: 'courseware-units/all',
+            userIsTeacher: 'userIsTeacher'
+        }),
+        units() {
+            return this.coursewareUnits.filter(unit => unit.relationships.range.data.id === this.context.id) ?? [];
+        },
+        hasUnits() {
+            return this.units.length > 0;
+        },
+        inCourseContext() {
+            return this.context.type === 'courses';
+        }
+    },
+    methods: {
+        ...mapActions({
+            setShowUnitAddDialog: 'setShowUnitAddDialog',
+        }),
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
new file mode 100644
index 0000000..7c9de01
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
@@ -0,0 +1,118 @@
+<template>
+    <div class="cw-unit-progress">
+        <p v-if="userIsTeacher">
+            {{
+                $gettext('Die Fortschrittsanzeige bezieht sich auf den Anteil der Teilnehmenden, die eine Seite aufgerufen haben.')
+            }}
+        </p>
+        <nav aria-label="Breadcrumb" class="cw-unit-progress-breadcrumb">
+            <a v-if="parent" href="#" :title="$gettext('Hauptseite')" @click="visitRoot">
+                <studip-icon shape="home" />
+            </a>
+            <a v-if="parent && parent.id !== rootId" href="#" :title="parent.name" @click="selectChapter(parent.id)">
+                | {{ parent.name }}
+            </a>
+        </nav>
+        <div v-if="selected" class="cw-unit-progress-chapter">
+            <h1>
+                <a
+                    :href="chapterUrl"
+                    :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name })"
+                >
+                    {{ selected.name }}
+                </a>
+            </h1>
+            <courseware-progress-circle
+                :title="selected.id === rootId ? $gettext('Gesamtes Lernmaterial') : $gettext('diese Seite inkl. darunter liegender Seiten')"
+                :value="parseInt(selected.progress.cumulative)"
+            />
+            <courseware-progress-circle
+                :title="$gettext('diese Seite')"
+                class="cw-unit-progress-current"
+                :value="parseInt(selected.progress.self)"
+            />
+        </div>
+        <div v-if="children.length > 0" class="cw-unit-progress-subchapter-list">
+            <courseware-unit-progress-item
+                v-for="chapter in children"
+                :key="chapter.id"
+                :name="chapter.name"
+                :value="chapter.progress.cumulative"
+                :chapterId="chapter.id"
+                @selectChapter="selectChapter"
+            />
+        </div>
+        <div v-else class="cw-unit-empty-info">
+            <courseware-companion-box
+                mood="sad"
+                :msgCompanion="$gettext('Diese Seite enthält keine darunter liegenden Seiten.')"
+            />
+        </div>
+    </div>
+</template>
+
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareUnitProgressItem from './CoursewareUnitProgressItem.vue';
+import CoursewareProgressCircle from './CoursewareProgressCircle.vue';
+import StudipIcon from '../../StudipIcon.vue';
+
+import { mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-unit-progress',
+    components: {
+        CoursewareCompanionBox,
+        CoursewareUnitProgressItem,
+        CoursewareProgressCircle,
+        StudipIcon,
+    },
+    props: {
+        progressData: Object,
+        unitId: String,
+        rootId: Number,
+    },
+    data() {
+        return {
+            selected: null,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            userIsTeacher: 'userIsTeacher',
+        }),
+        chapterUrl() {
+            return (
+                STUDIP.URLHelper.getURL('dispatch.php/course/courseware/courseware/' + this.unitId) +
+                '#/structural_element/' +
+                this.selected.id
+            );
+        },
+        parent() {
+            if (!this.selected?.parent_id) {
+                return null;
+            }
+
+            return this.progressData[this.selected.parent_id];
+        },
+        children() {
+            if (!this.selected) {
+                return [];
+            }
+
+            return Object.values(this.progressData).filter(({ parent_id }) => parent_id === this.selected.id);
+        },
+    },
+    methods: {
+        visitRoot() {
+            this.selected = this.progressData[this.rootId];
+        },
+        selectChapter(id) {
+            this.selected = this.progressData[id] ?? null;
+        },
+    },
+    mounted() {
+        this.visitRoot();
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitProgressItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitProgressItem.vue
new file mode 100644
index 0000000..b83a692
--- /dev/null
+++ b/resources/vue/components/courseware/unit/CoursewareUnitProgressItem.vue
@@ -0,0 +1,31 @@
+<template>
+    <a 
+        href="#"
+        class="cw-unit-progress-item"
+        :title="name"
+        @click="$emit('selectChapter', chapterId)"
+    >
+        <div class="cw-unit-progress-item-value">
+            <courseware-progress-circle :value="parseInt(value)" />
+        </div>
+        <div class="cw-unit-progress-item-description">
+            {{ name }}
+        </div>
+    </a>
+</template>
+
+<script>
+import CoursewareProgressCircle from './CoursewareProgressCircle.vue';
+
+export default {
+    name: 'courseware-unit-progress-item',
+    components: {
+        CoursewareProgressCircle,
+    },
+    props: {
+        name: String,
+        value: Number,
+        chapterId: Number,
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue
new file mode 100644
index 0000000..6543594
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue
@@ -0,0 +1,42 @@
+<template>
+    <sidebar-widget id="courseware-action-widget" :title="$gettext('Aktionen')" v-if="structuralElement">
+        <template #content>
+            <ul class="widget-list widget-links cw-action-widget">
+                <li v-if="canEdit" class="cw-action-widget-add">
+                    <button @click="addElement">
+                        {{ $gettext('Seite hinzufügen') }}
+                    </button>
+                </li>
+            </ul>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-action-widget',
+    props: ['structuralElement'],
+    components: {
+        SidebarWidget,
+    },
+    computed: {
+        canEdit() {
+            if (!this.structuralElement) {
+                return false;
+            }
+            return this.structuralElement.attributes['can-edit'];
+        },
+    },
+    methods: {
+        ...mapActions({
+            showElementAddDialog: 'showElementAddDialog',
+        }),
+        addElement() {
+            this.showElementAddDialog(true);
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue
new file mode 100644
index 0000000..eb2c05f
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue
@@ -0,0 +1,61 @@
+<template>
+    <sidebar-widget :title="$gettext('Aktivitäten')">
+        <template #content>
+            <div class="cw-filter-widget">
+                <form class="default" @submit.prevent="">
+                    <select v-model="typeFilter">
+                        <option value="all">
+                            {{ $gettext('Alle') }}
+                        </option>
+                        <option value="edited">
+                            {{ $gettext('Bearbeitet') }}
+                        </option>
+                        <option value="created">
+                            {{ $gettext('Erstellt') }}
+                        </option>
+                        <option value="answered">
+                            {{ $gettext('Feedback') }}
+                        </option>
+                        <option value="interacted">
+                            {{ $gettext('Kommentiert') }}
+                        </option>
+                        <option value="voided">
+                            {{ $gettext('Gelöscht') }}
+                        </option>
+                    </select>
+                </form>
+            </div>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-activities-widget-filter-type',
+    components: {
+        SidebarWidget
+    },
+    data() {
+        return {
+            typeFilter: 'all',
+        };
+    },
+    methods: {
+        ...mapActions({
+            setTypeFilter: 'setTypeFilter',
+        }),
+        filterType() {
+            this.setTypeFilter(this.typeFilter);
+        },
+    },
+    watch: {
+        typeFilter() {
+            this.filterType();
+        },
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue
new file mode 100644
index 0000000..dc10e17
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue
@@ -0,0 +1,58 @@
+<template>
+    <sidebar-widget :title="$gettext('Lernmaterial')">
+        <template #content>
+            <div class="cw-filter-widget">
+                <form class="default" @submit.prevent="">
+                    <select v-model="unitFilter">
+                        <option value="all">
+                            {{ $gettext('Alle') }}
+                        </option>
+                        <option v-for="unit in coursewareUnits" :key="unit.id" :value="unit.id">
+                                {{ getUnitTitle(unit) }}
+                        </option>
+                    </select>
+                </form>
+            </div>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-activities-widget-filter-unit',
+    components: {
+        SidebarWidget
+    },
+    data() {
+        return {
+            unitFilter: 'all'
+        };
+    },
+    computed: {
+        ...mapGetters({
+            getStructuralElementById: 'courseware-structural-elements/byId',
+            coursewareUnits: 'courseware-units/all',
+        }),
+    },
+    methods: {
+        ...mapActions({
+            setUnitFilter: 'setUnitFilter',
+        }),
+        filterUnit() {
+            this.setUnitFilter(this.unitFilter);
+        },
+        getUnitTitle(unit) {
+            return this.getStructuralElementById({id: unit.relationships['structural-element'].data.id }).attributes.title;
+        }
+    },
+    watch: {
+        unitFilter() {
+            this.filterUnit();
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareAdminActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareAdminActionWidget.vue
new file mode 100644
index 0000000..6c02699
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareAdminActionWidget.vue
@@ -0,0 +1,34 @@
+<template>
+    <ul class="widget-list widget-links cw-action-widget">
+        <li v-if="templatesView" class="cw-action-widget-add">
+            <a href="#"  @click.prevent="addTemplate">
+                <translate>Vorlage hinzufügen</translate>
+            </a>
+        </li>
+    </ul>
+</template>
+
+<script>
+import { mapGetters, mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-admin-action-widget',
+    computed: {
+        ...mapGetters({
+            adminViewMode: 'adminViewMode',
+            showAddTemplateDialog: 'showAddTemplateDialog'
+        }),
+        templatesView() {
+            return this.adminViewMode === 'templates';
+        }
+    },
+    methods: {
+        ...mapActions({
+            setShowAddTemplateDialog: 'showAddTemplateDialog'
+        }),
+        addTemplate() {
+            this.setShowAddTemplateDialog(true);
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue b/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue
new file mode 100644
index 0000000..81a8071
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue
@@ -0,0 +1,33 @@
+<template>
+  <ul class="widget-list widget-links sidebar-views cw-view-widget">
+        <li :class="{ active: templatesView }">
+            <a href="#" @click.prevent="setTemplatesView">
+                <translate>Vorlagen</translate>
+            </a>
+        </li>
+  </ul>
+</template>
+
+<script>
+import { mapGetters, mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-admin-view-widget',
+    computed: {
+        ...mapGetters({
+            adminViewMode: 'adminViewMode'
+        }),
+        templatesView() {
+            return this.adminViewMode === 'templates';
+        },
+    },
+    methods: {
+        ...mapActions({
+            setAdminViewMode: 'adminViewMode'
+        }),
+        setTemplatesView() {
+            this.setAdminViewMode('templates');
+        },
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue
new file mode 100644
index 0000000..5b0d5af
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue
@@ -0,0 +1,51 @@
+<template>
+    <sidebar-widget :title="$gettext('Neueste Einträge')">
+        <template #content>
+            <div class="cw-filter-widget">
+                <form class="default" @submit.prevent="">
+                    <select v-model="createdFilter" :aria-label="$gettext('Filter: Neueste Einträge')">
+                        <option value="all">
+                            {{ $gettext('unbeschränkt') }}
+                        </option>
+                        <option value="oneDay">
+                            {{ $gettext('einen Tag alt') }}
+                        </option>
+                        <option value="oneWeek">
+                            {{ $gettext('eine Woche alt') }}
+                        </option>
+                    </select>
+                </form>
+            </div>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-comments-overview-widget-filter-created',
+    components: {
+        SidebarWidget
+    },
+    data() {
+        return {
+            createdFilter: 'all',
+        };
+    },
+    methods: {
+        ...mapActions({
+            setCreatedFilter: 'setCreatedFilter',
+        }),
+        filterCreated() {
+            this.setCreatedFilter(this.createdFilter);
+        },
+    },
+    watch: {
+        createdFilter() {
+            this.filterCreated();
+        },
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue
new file mode 100644
index 0000000..4d92294
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue
@@ -0,0 +1,51 @@
+<template>
+    <sidebar-widget :title="$gettext('Listen')">
+        <template #content>
+            <div class="cw-filter-widget">
+                <form class="default" @submit.prevent="">
+                    <select v-model="typeFilter" :aria-label="$gettext('Filter: Listen')">
+                        <option value="all">
+                            {{ $gettext('Blöcke und Seiten') }}
+                        </option>
+                        <option value="blocks">
+                            {{ $gettext('Blöcke') }}
+                        </option>
+                        <option value="elements">
+                            {{ $gettext('Seiten') }}
+                        </option>
+                    </select>
+                </form>
+            </div>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-comments-overview-widget-filter-type',
+    components: {
+        SidebarWidget
+    },
+    data() {
+        return {
+            typeFilter: 'all',
+        };
+    },
+    methods: {
+        ...mapActions({
+            setTypeFilter: 'setTypeFilter',
+        }),
+        filterType() {
+            this.setTypeFilter(this.typeFilter);
+        },
+    },
+    watch: {
+        typeFilter() {
+            this.filterType();
+        },
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterUnit.vue b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterUnit.vue
new file mode 100644
index 0000000..9e01f2e
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterUnit.vue
@@ -0,0 +1,60 @@
+<template>
+        <sidebar-widget :title="$gettext('Lernmaterial')">
+        <template #content>
+            <div class="cw-filter-widget">
+                <form class="default" @submit.prevent="">
+                    <select v-model="unitFilter" :aria-label="$gettext('Filter: Lernmaterial')">
+                        <option value="all">
+                            {{ $gettext('Alle') }}
+                        </option>
+                        <option v-for="unit in sortedUnits" :key="unit.id" :value="unit.id">
+                            {{ getUnitName(unit) }}
+                        </option>
+                    </select>
+                </form>
+            </div>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-comments-overview-widget-filter-unit',
+    data() {
+        return {
+            unitFilter: 'all',
+        };
+    },
+    computed: {
+        ...mapGetters({
+            units: 'courseware-units/all',
+            elementById: 'courseware-structural-elements/byId',
+        }),
+        sortedUnits() {
+            let units = _.cloneDeep(this.units);
+            units = units.sort((a, b) => this.getUnitName(a) < this.getUnitName(b) ? -1 : 1);
+
+            return units;
+        }
+    },
+    methods: {
+        ...mapActions({
+            setUnitFilter: 'setUnitFilter',
+        }),
+        filterUnit() {
+            this.setUnitFilter(this.unitFilter);
+        },
+        getUnitName(unit) {
+            return this.elementById({ id: unit.relationships['structural-element'].data.id}).attributes.title;
+        }
+    },
+    watch: {
+        unitFilter() {
+            this.filterUnit();
+        },
+    }
+};
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue b/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue
new file mode 100644
index 0000000..b31ee20
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue
@@ -0,0 +1,95 @@
+<template>
+    <sidebar-widget :title="$gettext('Export')" v-if="structuralElement">
+        <template #content>
+            <ul class="widget-list widget-links cw-export-widget" v-if="structuralElement">
+                <li v-if="showExportArchiv" class="cw-export-widget-export">
+                    <button @click="exportElement">
+                        {{ $gettext('Seiten exportieren') }}
+                    </button>
+                </li>
+                <li v-if="showExportPdf" class="cw-export-widget-export-pdf">
+                    <button @click="pdfElement">
+                        {{ $gettext('PDF-Dokument erstellen') }}
+                    </button>
+                </li>
+                <li v-if="showOer" class="cw-export-widget-oer">
+                    <button @click="oerElement">
+                        {{ $gettext('Auf OER Campus veröffentlichen') }}
+                    </button>
+                </li>
+                <li v-if="!showExportArchiv && !showExportPdf && !showOer">
+                    {{ $gettext('Keine Exportoptionen verfügbar') }}
+                </li>
+            </ul>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+import CoursewareExport from '@/vue/mixins/courseware/export.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-export-widget',
+    props: ['structuralElement', 'canVisit'],
+    components: {
+        SidebarWidget
+    },
+    mixins: [CoursewareExport],
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            oerCampusEnabled: 'oerCampusEnabled',
+            userIsTeacher: 'userIsTeacher',
+        }),
+        canEdit() {
+            if (!this.structuralElement) {
+                return false;
+            }
+            return this.structuralElement.attributes['can-edit'];
+        },
+        showExportArchiv() {
+            if (this.context.type === 'users') {
+                return true;
+            }
+
+            return this.canEdit;
+        },
+        showExportPdf() {
+            if (this.context.type === 'users') {
+                return true;
+            }
+
+            return this.canVisit;
+        },
+        showOer() {
+            if (!this.oerCampusEnabled) {
+                return false;
+            }
+
+            if (this.context.type === 'users') {
+                return true;
+            }
+
+            return this.userIsTeacher && this.canVisit;
+        }
+    },
+    methods: {
+        ...mapActions({
+            showElementExportDialog: 'showElementExportDialog',
+            showElementPdfExportDialog: 'showElementPdfExportDialog',
+            showElementOerDialog: 'showElementOerDialog',
+        }),
+        exportElement() {
+            this.showElementExportDialog(true);
+        },
+        pdfElement() {
+            this.showElementPdfExportDialog(true);
+        },
+        oerElement() {
+            this.showElementOerDialog(true);
+        }
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue b/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue
new file mode 100644
index 0000000..c0bfd5b
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue
@@ -0,0 +1,60 @@
+<template>
+    <sidebar-widget :title="$gettext('Import')">
+        <template #content>
+            <ul class="widget-list widget-links cw-import-widget">
+                <li class="cw-import-widget-archive">
+                    <button @click="importElements">
+                        {{ $gettext('Seiten importieren') }}
+                    </button>
+                </li>
+                <li class="cw-import-widget-copy">
+                    <button @click="copyElements">
+                        {{ $gettext('Seiten kopieren') }}
+                    </button>
+                </li>
+                <li v-if="inCourseContext && userIsTeacher" class="cw-action-widget-link">
+                    <button @click="linkElement">
+                        {{ $gettext('Seiten verknüpfen') }}
+                    </button>
+                </li>
+            </ul>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-import-widget',
+    components: {
+        SidebarWidget,
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            userIsTeacher: 'userIsTeacher',
+        }),
+        inCourseContext() {
+            return this.context.type === 'courses';
+        }
+    },
+    methods: {
+        ...mapActions({
+            showElementImportDialog: 'showElementImportDialog',
+            showElementCopyDialog: 'showElementCopyDialog',
+            showElementLinkDialog: 'showElementLinkDialog',
+        }),
+        importElements() {
+            this.showElementImportDialog(true);
+        },
+        copyElements() {
+            this.showElementCopyDialog(true);
+        },
+        linkElement() {
+            this.showElementLinkDialog(true);
+        },
+    },
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue b/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue
new file mode 100644
index 0000000..aed84b5
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue
@@ -0,0 +1,94 @@
+<template>
+   <sidebar-widget :title="$gettext('Suche')">
+        <template #content>
+            <form class="sidebar-search" @submit.prevent="">
+                <ul class="needles">
+                    <li>
+                        <div class="input-group files-search">
+                            <input
+                                type="text"
+                                v-model="searchTerm"
+                                :aria-label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')"
+                            />
+                            <a v-if="showSearchResults" @click.prevent="setShowSearchResults(false)"
+                                class="reset-search">
+                                <studip-icon shape="decline" :size="20"></studip-icon>
+                            </a>
+                            <button
+                                type="submit"
+                                :value="$gettext('Suchen')"
+                                aria-controls="search"
+                                class="submit-search"
+                                @click="loadResults"
+                            >
+                                <studip-icon shape="search" :size="20"></studip-icon>
+                            </button>
+                        </div>
+                    </li>
+                </ul>
+            </form>
+        </template>
+   </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+import StudipIcon from '../../StudipIcon.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+import axios from 'axios';
+
+export default {
+    name: 'courseware-search-widget',
+    components: {
+        StudipIcon,
+        SidebarWidget,
+    },
+    data() {
+        return {
+            searchTerm: ''
+        }
+    },
+    computed: {
+        ...mapGetters({
+            courseware: 'courseware',
+            context: 'context',
+            showSearchResults: 'showSearchResults'
+        }),
+    },
+    methods: {
+        ...mapActions({
+            setShowSearchResults: 'setShowSearchResults',
+            setSearchResults: 'setSearchResults',
+            companionWarning: 'companionWarning',
+            companionError: 'companionError'
+        }),
+        loadResults() {
+            if (this.searchTerm.length < 3) {
+                this.companionWarning({ info: this.$gettext('Leider ist Ihr Suchbegriff zu kurz. Der Suchbegriff muss mindestens 3 Zeichen lang sein.')});
+                return;
+            }
+            const limit = 100;
+            let params = {
+                search: this.searchTerm,
+                filters: { category: 'GlobalSearchCourseware', contextType: this.context.type, rangeId: this.context.id}
+            };
+
+            axios({
+                method: 'get',
+                url: STUDIP.URLHelper.getURL('dispatch.php/globalsearch/find/' + limit),
+                params: params,
+            }).then( result => {
+                this.setShowSearchResults(true);
+                if (result.data.GlobalSearchCourseware) {
+                    this.setSearchResults((result.data.GlobalSearchCourseware.content));
+                } else {
+                    this.setSearchResults([]);
+                }
+            }).catch(error => {
+                this.companionError({ info: this.$gettext('Bei der Anfrage ist ein Fehler aufgetreten.')});
+            });
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue
new file mode 100644
index 0000000..c7232c3
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue
@@ -0,0 +1,30 @@
+<template>
+    <sidebar-widget id="courseware-action-widget"  :title="$gettext('Aktionen')">
+        <template #content>
+            <ul class="widget-list widget-links cw-action-widget">
+                <li class="cw-action-widget-add">
+                    <button @click="setShowUnitAddDialog(true)">
+                        {{ $gettext('Lernmaterial hinzufügen') }}
+                    </button>
+                </li>
+            </ul>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-shelf-action-widget',
+    components: {
+        SidebarWidget
+    },
+    methods: {
+        ...mapActions({
+            setShowUnitAddDialog: 'setShowUnitAddDialog',
+        }),
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue b/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue
new file mode 100644
index 0000000..2270437
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue
@@ -0,0 +1,37 @@
+<template>
+    <sidebar-widget :title="$gettext('Import')">
+        <template #content>
+            <ul class="widget-list widget-links cw-import-widget">
+                <li class="cw-import-widget-archive">
+                    <button @click="setShowUnitImportDialog(true)">
+                        {{ $gettext('Lernmaterial importieren') }}
+                    </button>
+                </li>
+                <li class="cw-import-widget-copy">
+                    <button @click="setShowUnitCopyDialog(true)">
+                        {{ $gettext('Lernmaterial kopieren') }}
+                    </button>
+                </li>
+            </ul>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-shelf-import-widget',
+    components: {
+        SidebarWidget,
+    },
+    methods: {
+        ...mapActions({
+            setShowUnitCopyDialog: 'setShowUnitCopyDialog',
+            setShowUnitImportDialog: 'setShowUnitImportDialog',
+        }),
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
new file mode 100644
index 0000000..c2f54e9
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
@@ -0,0 +1,31 @@
+<template>
+    <sidebar-widget id="courseware-action-widget" :title="$gettext('Aktionen')">
+        <template #content>
+            <ul class="widget-list widget-links cw-action-widget">
+                <li class="cw-action-widget-add">
+                    <button @click="setShowTasksDistributeDialog(true)">
+                        {{ $gettext('Aufgabe verteilen') }}
+                    </button>
+                </li>
+            </ul>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+
+import { mapActions } from 'vuex';
+
+export default {
+    name: 'courseware-tasks-action-widget',
+    components: {
+        SidebarWidget,
+    },
+    methods: {
+        ...mapActions({
+            setShowTasksDistributeDialog: 'setShowTasksDistributeDialog',
+        }),
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue b/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue
new file mode 100644
index 0000000..5202abd
--- /dev/null
+++ b/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue
@@ -0,0 +1,81 @@
+<template>
+    <sidebar-widget :title="$gettext('Ansichten')" v-if="structuralElement">
+        <template #content>
+            <ul class="widget-list widget-links sidebar-views cw-view-widget">
+                <li :class="{ active: readView }">
+                    <button @click="setReadView">
+                        <translate>Lesen</translate>
+                    </button>
+                </li>
+                <li
+                    v-if="canEdit"
+                    :class="{ active: editView }"
+                >
+                    <button @click="setEditView">
+                        <translate>Bearbeiten</translate>
+                    </button>
+                </li>
+                <li
+                    v-if="context.type === 'courses' && canVisit"
+                    :class="{ active: discussView }"
+                >
+                    <button @click="setDiscussView">
+                        <translate>Kommentieren</translate>
+                    </button>
+                </li>
+            </ul>
+        </template>
+    </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../../SidebarWidget.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-view-widget',
+    props: ['structuralElement', 'canVisit'],
+    components: {
+        SidebarWidget
+    },
+    computed: {
+        ...mapGetters({
+            viewMode: 'viewMode',
+            context: 'context',
+        }),
+        readView() {
+            return this.viewMode === 'read';
+        },
+        editView() {
+            return this.viewMode === 'edit';
+        },
+        discussView() {
+            return this.viewMode === 'discuss';
+        },
+        canEdit() {
+            if (!this.structuralElement) {
+                return false;
+            }
+            return this.structuralElement.attributes['can-edit'];
+        },
+    },
+    methods: {
+        ...mapActions({
+            coursewareViewMode: 'coursewareViewMode',
+            coursewareBlockAdder: 'coursewareBlockAdder',
+            setToolbarItem: 'coursewareSelectedToolbarItem',
+        }),
+        setReadView() {
+            this.coursewareViewMode('read');
+            this.setToolbarItem('contents');
+            this.coursewareBlockAdder({});
+        },
+        setEditView() {
+            this.coursewareViewMode('edit');
+        },
+        setDiscussView() {
+            this.coursewareViewMode('discuss');
+        },
+    },
+};
+</script>
diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js
index 5e2ea1f..3309b55 100644
--- a/resources/vue/courseware-index-app.js
+++ b/resources/vue/courseware-index-app.js
@@ -1,6 +1,6 @@
 import CoursewareModule from './store/courseware/courseware.module';
 import CoursewareStructureModule from './store/courseware/structure.module';
-import CoursewareStructuralElement from './components/courseware/CoursewareStructuralElement.vue';
+import CoursewareStructuralElement from './components/courseware/structural-element/CoursewareStructuralElement.vue';
 import IndexApp from './components/courseware/IndexApp.vue';
 import PluginManager from './components/courseware/plugin-manager.js';
 import Vue from 'vue';
diff --git a/resources/vue/courseware-public-app.js b/resources/vue/courseware-public-app.js
index ae70605..1d50b8a 100644
--- a/resources/vue/courseware-public-app.js
+++ b/resources/vue/courseware-public-app.js
@@ -1,6 +1,6 @@
 import PublicApp from './components/courseware/PublicApp.vue';
 import CoursewarePublicModule from './store/courseware/courseware-public.module';
-import PublicCoursewareStructuralElement from './components/courseware/PublicCoursewareStructuralElement.vue';
+import PublicCoursewareStructuralElement from './components/courseware/structural-element/PublicCoursewareStructuralElement.vue';
 import CoursewarePublicStructureModule from './store/courseware/public-structure.module';
 import PluginManager from './components/courseware/plugin-manager.js';
 import VueRouter from 'vue-router';
diff --git a/resources/vue/mixins/courseware/block.js b/resources/vue/mixins/courseware/block.js
new file mode 100644
index 0000000..0a83906
--- /dev/null
+++ b/resources/vue/mixins/courseware/block.js
@@ -0,0 +1,37 @@
+import { mapActions, mapGetters } from 'vuex';
+
+const blockMixin = {
+    computed: {
+        ...mapGetters({
+            getUserProgress: 'courseware-user-progresses/related',
+        }),
+        userProgress: {
+            get: function () {
+                return this.getUserProgress({ parent: this.block, relationship: 'user-progress' });
+            },
+            set: function (grade) {
+                this.userProgress.attributes.grade = grade;
+
+                return this.updateUserProgress(this.userProgress);
+            },
+        },
+    },
+    methods: {
+        ...mapActions({
+            updateUserProgress: 'courseware-user-progresses/update',
+        }),
+        getReadableDate(date) {
+            let locale = navigator.language ? navigator.language : 'de-DE';
+            return new Date(date).toLocaleDateString(locale, {
+                year: "numeric",
+                month: "2-digit",
+                day: "2-digit",
+            });
+        },
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
+    },
+};
+
+export default blockMixin;
\ No newline at end of file
diff --git a/resources/vue/mixins/courseware/content-icons.js b/resources/vue/mixins/courseware/content-icons.js
new file mode 100644
index 0000000..4f86edf
--- /dev/null
+++ b/resources/vue/mixins/courseware/content-icons.js
@@ -0,0 +1,122 @@
+const contentIconsMixin = {
+    computed: {
+        contentIcons() {
+            return [
+                'accept',
+                'add',
+                'add-circle',
+                'admin',
+                'aladdin',
+                'arr_1down',
+                'arr_1left',
+                'arr_1right',
+                'arr_1up',
+                'arr_2down',
+                'arr_2left',
+                'arr_2right',
+                'arr_2up',
+                'audio',
+                'audio3',
+                'billboard',
+                'block-canvas',
+                'block-comparison',
+                'block-eyecatcher',
+                'block-gallery',
+                'block-gallery2',
+                'block-imagemap',
+                'brainstorm',
+                'campusnavi',
+                'chat2',
+                'code',
+                'community2',
+                'computer',
+                'consultation',
+                'content',
+                'courseware',
+                'crown',
+                'date-single',
+                'decline',
+                'decline-circle',
+                'doctoral_cap',
+                'download',
+                'dropbox',
+                'edit',
+                'exclaim',
+                'exclaim-circle',
+                'export',
+                'favorite',
+                'filter',
+                'globe',
+                'graph',
+                'group2',
+                'group3',
+                'group4',
+                'home',
+                'home2',
+                'info',
+                'info-circle',
+                'install',
+                'institute',
+                'key',
+                'knife',
+                'learnmodule',
+                'lightbulb',
+                'lightbulb2',
+                'link2',
+                'link3',
+                'link-extern',
+                'link-intern',
+                'literature',
+                'lock-locked',
+                'lock-unlocked',
+                'mail2',
+                'medal',
+                'metro',
+                'microphone',
+                'module',
+                'network',
+                'network2',
+                'notification',
+                'notification2',
+                'opencast',
+                'outer-space',
+                'permalink',
+                'person',
+                'phone',
+                'picture',
+                'place',
+                'plugin',
+                'question',
+                'question-circle',
+                'ranking',
+                'remove',
+                'remove-circle',
+                'resources',
+                'roles',
+                'schedule2',
+                'search',
+                'settings',
+                'span-empty',
+                'span-1quarter',
+                'span-2quarter',
+                'span-3quarter',
+                'span-full',
+                'spiral',
+                'sport',
+                'staple',
+                'star',
+                'star-empty',
+                'star-halffull',
+                'test',
+                'tools',
+                'topic',
+                'ufo',
+                'video2',
+                'visibility-visible',
+                'wizard',
+            ];
+        },
+    },
+};
+
+export default contentIconsMixin;
-- 
cgit v1.0

