From e09c643e2ed3719b081ce3ee3b55fb15cbd04274 Mon Sep 17 00:00:00 2001
From: Ron Lucke <lucke@elan-ev.de>
Date: Mon, 18 Dec 2023 11:46:01 +0000
Subject: [PATCH] TIC #2532

Merge request studip/studip!2313
---
 .../assets/stylesheets/scss/courseware.scss   |   3 +
 .../scss/courseware/blockadder.scss           |  71 +++--
 .../scss/courseware/containers/accordion.scss |  23 ++
 .../containers/default-container.scss         |  23 +-
 .../scss/courseware/containers/list.scss      |  19 ++
 .../scss/courseware/containers/tabs.scss      |  23 ++
 .../stylesheets/scss/courseware/sortable.scss | 110 +++++++-
 .../blocks/CoursewareBeforeAfterBlock.vue     |  27 +-
 .../blocks/CoursewareCanvasBlock.vue          |  20 +-
 .../blocks/CoursewareChartBlock.vue           |   2 +-
 .../blocks/CoursewareDocumentBlock.vue        |  12 +-
 .../CoursewareAccordionContainer.vue          |  39 +--
 .../CoursewareContainerAdderItem.vue          |  52 ----
 .../containers/CoursewareDefaultContainer.vue |   4 +-
 .../containers/CoursewareListContainer.vue    |  11 +-
 .../containers/CoursewareTabsContainer.vue    |  28 +-
 .../containers/container-components.js        |   2 +
 .../CoursewareBlockadderItem.vue              | 115 --------
 .../CoursewareClipboardItem.vue               | 246 ------------------
 .../CoursewareStructuralElement.vue           |   6 +-
 .../toolbar/CoursewareBlockadderItem.vue      |  45 +---
 .../toolbar/CoursewareClipboardItem.vue       | 101 +------
 .../toolbar/CoursewareContainerAdderItem.vue  |  60 +++--
 .../courseware/toolbar/CoursewareToolbar.vue  |  19 ++
 .../toolbar/CoursewareToolbarBlocks.vue       | 152 ++++++++---
 .../toolbar/CoursewareToolbarClipboard.vue    | 143 ++++++++--
 .../toolbar/CoursewareToolbarContainers.vue   | 172 +++++++++---
 resources/vue/mixins/courseware/clipboard.js  | 125 +++++++++
 resources/vue/mixins/courseware/container.js  | 197 +++++++++++++-
 29 files changed, 1083 insertions(+), 767 deletions(-)
 delete mode 100644 resources/vue/components/courseware/containers/CoursewareContainerAdderItem.vue
 delete mode 100644 resources/vue/components/courseware/structural-element/CoursewareBlockadderItem.vue
 delete mode 100644 resources/vue/components/courseware/structural-element/CoursewareClipboardItem.vue
 create mode 100644 resources/vue/mixins/courseware/clipboard.js

diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss
index 8fdce408649..6c3d7ea620e 100644
--- a/resources/assets/stylesheets/scss/courseware.scss
+++ b/resources/assets/stylesheets/scss/courseware.scss
@@ -15,6 +15,9 @@
 @import './courseware/shelf.scss';
 @import './courseware/structural-element.scss';
 @import './courseware/containers/default-container.scss';
+@import './courseware/containers/accordion.scss';
+@import './courseware/containers/list.scss';
+@import './courseware/containers/tabs.scss';
 @import './courseware/blocks/default-block.scss';
 
 @import './courseware/layouts/collapsible.scss';
diff --git a/resources/assets/stylesheets/scss/courseware/blockadder.scss b/resources/assets/stylesheets/scss/courseware/blockadder.scss
index 774c3d4a1a0..ad8424b9071 100644
--- a/resources/assets/stylesheets/scss/courseware/blockadder.scss
+++ b/resources/assets/stylesheets/scss/courseware/blockadder.scss
@@ -101,13 +101,21 @@
         border: solid thin var(--content-color-40);
         max-width: 268px;
 
+        .cw-sortable-handle {
+            opacity: 0;
+        }
+
         &:hover {
             border-color: var(--base-color);
+
+            .cw-sortable-handle {
+                opacity: 1;
+            }
         }
         .cw-blockadder-item {
             padding: 64px 10px 4px 10px;
             @include background-icon(unit-test, clickable, 48);
-            background-position: 10px 10px;
+            background-position: 16px 10px;
             background-repeat: no-repeat;
             cursor: pointer;
 
@@ -116,7 +124,6 @@
                     @include background-icon($icon, clickable, 48);
                 }
             }
-            .cw-clipboard-item-title,
             .cw-blockadder-item-title {
                 display: inline-block;
                 font-weight: 600;
@@ -124,7 +131,7 @@
             }
             .cw-blockadder-item-description {
                 display: inline-block;
-                margin: 0 0 4px;
+                margin: 0 0 4px;   
             }
         }
         .cw-blockadder-item-fav {
@@ -184,30 +191,42 @@
     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;
+.cw-containeradder-item-wrapper {
     border: solid thin var(--content-color-40);
-    cursor: pointer;
+    .cw-sortable-handle {
+        opacity: 0;
+    }
 
     &:hover {
         border-color: var(--base-color);
-    }
 
-    @each $item, $icon in $containeradder-items {
-        &.cw-containeradder-item-#{$item} {
-            @include background-icon($icon, clickable, 48);
+        .cw-sortable-handle {
+            opacity: 1;
         }
     }
 
-    .cw-containeradder-item-title {
-        font-weight: 600;
+    .cw-containeradder-item {
+        margin-bottom: 4px;
+        padding: 1em 1em 1em 6em;
+        @include background-icon(unit-test, clickable, 48);
+        background-position: 16px center;
+        background-repeat: no-repeat;
+        cursor: pointer;
+
+        @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;
@@ -264,15 +283,23 @@
         border: solid thin var(--content-color-40);
         max-width: 248px;
 
+        .cw-sortable-handle {
+            opacity: 0;
+        }
+
         &:hover {
             border-color: var(--base-color);
+
+            .cw-sortable-handle {
+                opacity: 1;
+            }
         }
 
         .cw-clipboard-item {
             width: calc(100% - 36px);
             padding: 64px 10px 4px 10px;
             @include background-icon(unit-test, clickable, 48);
-            background-position: 10px 10px;
+            background-position: 16px 10px;
             background-repeat: no-repeat;
             cursor: pointer;
             background-color: var(--white);
@@ -296,6 +323,16 @@
                 font-weight: 600;
                 margin-bottom: 2px;
             }
+
+            .cw-clipboard-item-description {
+                display: -webkit-box;
+                margin: 0 0 4px;
+                max-height: 4em;
+                word-break: break-word;
+                overflow: hidden;
+                -webkit-line-clamp: 3;
+                -webkit-box-orient: vertical;
+            }
         }
         .cw-clipboard-item-action-menu-wrapper {
             padding: 8px;
diff --git a/resources/assets/stylesheets/scss/courseware/containers/accordion.scss b/resources/assets/stylesheets/scss/courseware/containers/accordion.scss
index e69de29bb2d..44cb56151f0 100644
--- a/resources/assets/stylesheets/scss/courseware/containers/accordion.scss
+++ b/resources/assets/stylesheets/scss/courseware/containers/accordion.scss
@@ -0,0 +1,23 @@
+.cw-container-accordion {
+
+    .cw-block-wrapper-active {
+        .cw-container-accordion-block-list:empty {
+            height: 4em;
+            border: dashed 2px var(--content-color-40);
+        }
+    }
+
+    .cw-collapsible-content > .cw-companion-box {
+        border: none;
+        margin-bottom: 0;
+    }
+
+    .cw-container-accordion-block-list {
+        list-style: none;
+        padding: 0;
+
+        &.cw-container-accordion-sort-mode {
+            padding: 8px 0 0 0;
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss
index 489a1fa0ea5..7798d502dd9 100644
--- a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss
+++ b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss
@@ -75,31 +75,16 @@
 
         &.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;
+            &:last-child {
+                margin: 0;
+            }
+        }
     }
 }
 
diff --git a/resources/assets/stylesheets/scss/courseware/containers/list.scss b/resources/assets/stylesheets/scss/courseware/containers/list.scss
index e69de29bb2d..063addaa0fd 100644
--- a/resources/assets/stylesheets/scss/courseware/containers/list.scss
+++ b/resources/assets/stylesheets/scss/courseware/containers/list.scss
@@ -0,0 +1,19 @@
+.cw-container-list {
+    .cw-block-wrapper-active {
+
+        >.cw-companion-box {
+            border: none;
+            margin-bottom: 0;
+        }
+
+        .cw-container-list-block-list:empty {
+            height: 4em;
+            border: dashed 2px var(--content-color-40);
+        }
+    }
+
+    .cw-container-list-block-list {
+        padding: 0;
+        list-style: none;
+    }
+}
\ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/containers/tabs.scss b/resources/assets/stylesheets/scss/courseware/containers/tabs.scss
index e69de29bb2d..bb3760580e6 100644
--- a/resources/assets/stylesheets/scss/courseware/containers/tabs.scss
+++ b/resources/assets/stylesheets/scss/courseware/containers/tabs.scss
@@ -0,0 +1,23 @@
+.cw-container-tabs {
+    .cw-tab-active > .cw-companion-box {
+        border: none;
+        margin-bottom: 0;
+    }
+
+    .cw-container-tabs-block-list {
+        list-style: none;
+        padding: 4px 0;
+    }
+
+    .cw-block-wrapper-active {
+
+        .cw-container-tabs-block-list:empty {
+            height: 4em;
+            border: dashed 2px var(--content-color-40);
+        }
+
+        .cw-tabs-content {
+            padding: 8px 0 0 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
index 9f90de4e827..7eaccdad97c 100644
--- a/resources/assets/stylesheets/scss/courseware/sortable.scss
+++ b/resources/assets/stylesheets/scss/courseware/sortable.scss
@@ -7,6 +7,47 @@
     &.cw-sortable-handle-dragging {
         cursor: grabbing;
     }
+    &.cw-sortable-handle-blocks {
+        position: relative;
+        top: -42px;
+        left: -2px;
+        margin-right: -15px;
+        margin-bottom: -10px;
+    }
+    &.cw-sortable-handle-containers {
+        position: absolute;
+        left: 0;
+        top: 50%;
+        margin-top: -12px;
+    }
+
+    &.cw-sortable-handle-blockadder {
+        display: block;
+        position: absolute;
+        padding: 8px 64px 24px 4px;
+        margin: 8px 4px;
+        background-position: top left;
+    }
+
+    &.cw-sortable-handle-containeradder {
+        display: block;
+        position: absolute;
+        padding: 8px 64px 48px 4px;
+        margin: 8px 4px;
+        background-position: top left;
+    }
+
+    &.cw-sortable-handle-clipboard {
+        display: block;
+        position: absolute;
+        padding: 8px 64px 24px 4px;
+        margin: 8px 4px;
+        background-position: top left;
+    }
+}
+
+.cw-container-dragitem {
+    display: none;
 }
 
 .cw-block-item-sortable {
@@ -29,12 +70,6 @@
     }
 }
 
-.container-ghost,
-.block-ghost {
-    opacity: 0.6;
-}
-
-
 .cw-container-wrapper-edit {
     width: calc(100% - 64px);
 
@@ -56,4 +91,67 @@
     .cw-container-header {
         font-style: italic;
     }
+}
+
+.cw-sortable-handle-blocks {
+    position: relative;
+}
+
+.cw-container-dragitem {
+    display: none;
+}
+
+.container-ghost {
+    background: var(--white);
+    border: dashed 2px var(--content-color-40);
+    margin-top: -5px;
+    margin-bottom: 15px;
+    
+    a {
+        opacity: 0;
+    }
+
+    button {
+        opacity: 0;
+    }
+
+    &.cw-clipboard-item-wrapper {
+        .cw-clipboard-item-action-menu-wrapper {
+            opacity: 0;
+        }
+    }
+
+    .cw-sortable-handle,
+    .cw-container {
+        opacity: 0;
+    }
+}
+
+.block-ghost {
+    background: var(--white);
+    border: dashed 2px var(--content-color-40);
+    padding-top: 5px;
+    padding-left: 5px;
+    height: 100px;
+    margin-bottom: 15px;
+    margin-top: -5px;
+
+    a {
+        opacity: 0;
+    }
+
+    button {
+        opacity: 0;
+    }
+
+    &.cw-clipboard-item-wrapper {
+        .cw-clipboard-item-action-menu-wrapper {
+            opacity: 0;
+        }
+    }
+
+    .cw-sortable-handle,
+    .cw-block {
+        opacity: 0;
+    }
 }
\ No newline at end of file
diff --git a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
index 2c6ac693d88..8d4e496403b 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
@@ -152,22 +152,25 @@ export default {
         },
     },
     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 (this.block.id) {
+            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];
+                    if (response[i].id === this.afterFileId) {
+                        this.afterFile = response[i];
+                    }
                 }
-            }
 
-            this.currentBeforeFile = this.beforeFile;
-            this.currentAfterFile = this.afterFile;
-        });
+                this.currentBeforeFile = this.beforeFile;
+                this.currentAfterFile  = this.afterFile;
+            });
 
-        this.loadImages();
+            this.loadImages();
+        }
+        
         this.initCurrentData();
     },
     methods: {
diff --git a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
index 794e4a9adce..d26446825d9 100644
--- a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
@@ -322,13 +322,15 @@ export default {
         }
     },
     mounted() {
-        this.loadFileRefs(this.block.id).then((response) => {
-            this.file = response[0];
-            this.currentFile = this.file;
-            this.initCurrentData();
-            this.buildCanvas();
-        });
-        this.loadImageFile();
+        if (this.block.id) {
+            this.loadFileRefs(this.block.id).then((response) => {
+                this.file = response[0];
+                this.currentFile = this.file;
+                this.initCurrentData();
+                this.buildCanvas();
+            });
+            this.loadImageFile();
+        }
     },
     methods: {
         ...mapActions({
@@ -632,7 +634,9 @@ export default {
             data.attributes.payload.canvas_draw.clickTool = JSON.stringify(this.clickTool);
             data.attributes.payload.canvas_draw.Text = JSON.stringify(this.Text);
 
-            await this.updateUserDataFields(data);
+            if (data.id) {
+                await this.updateUserDataFields(data);
+            }
         },
         storeBlock() {
             let attributes = {};
diff --git a/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue b/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue
index 42d2d8156b0..fa85a4119e3 100644
--- a/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue
@@ -172,7 +172,7 @@ export default {
             updateBlock: 'updateBlockInContainer',
         }),
         initCurrentData() {
-            this.currentContent = this.content;
+            this.currentContent = this.content || [];
             this.currentLabel = this.label;
             this.currentType = this.type;
             this.setItemTab = 0;
diff --git a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
index 7f7b10ce003..0af177e27c5 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
@@ -371,11 +371,13 @@ export default {
         },
     },
     mounted() {
-        this.loadFileRefs(this.block.id).then((response) => {
-            this.file = response[0];
-            this.currentFile = this.file;
-            this.initPdfTask();
-        });
+        if (this.block.id) {
+            this.loadFileRefs(this.block.id).then((response) => {
+                this.file = response[0];
+                this.currentFile = this.file;
+                this.initPdfTask();
+            });
+        }
         this.initCurrentData();
     },
     methods: {
diff --git a/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue
index 3ee9a96b222..c45f03af110 100644
--- a/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue
@@ -35,10 +35,15 @@
                 </ul>
                 <template v-else>
                     <template v-if="!processing">
+                        <courseware-companion-box
+                            v-if="section.blocks.length === 0"
+                            mood="pointing"
+                            :msgCompanion="$gettext('Dieses Fach enthält keine Blöcke.')">
+                        </courseware-companion-box>
                         <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' : '']"
+                            class="cw-container-accordion-block-list cw-container-accordion-sort-mode"
+                            :class="[section.blocks.length === 0 ? 'cw-container-accordion-sort-mode-empty' : '']"
                             tag="ol"
                             role="listbox"
                             v-model="section.blocks"
@@ -46,7 +51,6 @@
                             handle=".cw-sortable-handle"
                             group="blocks"
                             @start="isDragging = true"
-                            @end="dropBlock"
                             :containerId="container.id"
                             :sectionId="index"
                         >
@@ -70,9 +74,6 @@
                                 />
                             </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...')" />
@@ -184,7 +185,7 @@ export default {
             return this.viewMode === 'edit';
         },
         blocks() {
-            if (!this.container) {
+            if (!this.container || this.container.newContainer) {
                 return [];
             }
 
@@ -211,13 +212,14 @@ export default {
             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 (sections) {
+                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);
+                        }
                     }
                 }
             }
@@ -417,7 +419,14 @@ export default {
             }
         },
         currentSections: {
-            handler() {
+            handler(newSections, oldSections) {
+                if (oldSections.length > 0 && 
+                    newSections[oldSections.length -1].blocks.length > oldSections[oldSections.length - 1].blocks.length) {
+                        this.$emit('blockAdded');
+                        this.$nextTick(() => {
+                            this.sortInSlots.push(oldSections.length - 1);
+                        });
+                }
                 if (this.keyboardSelected) {
                     this.$nextTick(() => {
                         this.$refs['sortableHandle' + this.keyboardSelected][0].focus();
diff --git a/resources/vue/components/courseware/containers/CoursewareContainerAdderItem.vue b/resources/vue/components/courseware/containers/CoursewareContainerAdderItem.vue
deleted file mode 100644
index 78cc4eda3bc..00000000000
--- a/resources/vue/components/courseware/containers/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/containers/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
index 1fa05f50ae8..ad60b1453fc 100644
--- a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
@@ -1,7 +1,7 @@
 <template>
     <div
-        class="cw-container cw-container-list"
-        :class="['cw-container-colspan-' + colSpan, showEditMode && canEdit ? 'cw-container-active' : '']"
+        class="cw-container"
+        :class="['cw-container-colspan-' + colSpan, showEditMode && canEdit ? 'cw-container-active' : '', containerClass]"
     >
         <div class="cw-container-content">
             <header v-if="showEditMode && canEdit" class="cw-container-header" :class="{ 'cw-container-header-open': isOpen }">
diff --git a/resources/vue/components/courseware/containers/CoursewareListContainer.vue b/resources/vue/components/courseware/containers/CoursewareListContainer.vue
index 6d8089f61a0..7f23210676a 100644
--- a/resources/vue/components/courseware/containers/CoursewareListContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareListContainer.vue
@@ -18,6 +18,11 @@
                     <span id="operation" class="assistive-text">
                         {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
                     </span>
+                    <courseware-companion-box
+                        v-if="empty"
+                        mood="pointing"
+                        :msgCompanion="$gettext('Dieser Abschnitt enthält keine Blöcke.')">
+                    </courseware-companion-box>
                     <draggable
                         v-if="showEditMode && canEdit"
                         class="cw-container-list-block-list cw-container-list-sort-mode"
@@ -57,7 +62,6 @@
                             />
                         </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...')" />
@@ -124,7 +128,7 @@ export default {
             return this.viewMode === 'edit';
         },
         blocks() {
-            if (!this.container) {
+            if (!this.container || this.container.newContainer) {
                 return [];
             }
             let containerBlocks = this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })).filter(Boolean);
@@ -135,6 +139,9 @@ export default {
 
             return sortedBlocks.concat(unallocatedBlocks);
         },
+        empty() {
+            return this.blockList.length === 0;
+        }
     },
     methods: {
         ...mapActions({
diff --git a/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue b/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue
index 725c017eaab..391cc820e91 100644
--- a/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue
@@ -45,9 +45,14 @@
                     </ul>
                     <template v-else>
                         <template v-if="canEdit">
+                            <courseware-companion-box
+                                v-if="section.blocks.length === 0"
+                                mood="pointing"
+                                :msgCompanion="$gettext('Dieses Fach enthält keine Blöcke.')">
+                            </courseware-companion-box>
                             <draggable
-                                class="cw-container-list-block-list cw-container-list-sort-mode"
-                                :class="[section.blocks.length === 0 ? 'cw-container-list-sort-mode-empty' : '']"
+                                class="cw-container-tabs-block-list cw-container-tabs-sort-mode"
+                                :class="[section.blocks.length === 0 ? 'cw-container-tabs-sort-mode-empty' : '']"
                                 tag="ol"
                                 role="listbox"
                                 v-model="section.blocks"
@@ -79,9 +84,6 @@
                                     />
                                 </li>
                             </draggable>
-                            <template v-if="canAddElements">
-                                <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/>
-                            </template>
                         </template>
                     </template>
                 </courseware-tab>
@@ -187,7 +189,7 @@ export default {
             return this.viewMode === 'edit';
         },
         blocks() {
-            if (!this.container) {
+            if (!this.container || this.container.newContainer) {
                 return [];
             }
 
@@ -215,12 +217,14 @@ export default {
 
             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 (sections) {
+                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);
+                        }
                     }
                 }
             }
diff --git a/resources/vue/components/courseware/containers/container-components.js b/resources/vue/components/courseware/containers/container-components.js
index 03c98b43592..3bb48dc926f 100644
--- a/resources/vue/components/courseware/containers/container-components.js
+++ b/resources/vue/components/courseware/containers/container-components.js
@@ -32,6 +32,7 @@ import CoursewareTimelineBlock from '../blocks/CoursewareTimelineBlock.vue';
 import CoursewareTypewriterBlock from '../blocks/CoursewareTypewriterBlock.vue';
 import CoursewareVideoBlock from '../blocks/CoursewareVideoBlock.vue';
 //layout
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import StudipIcon from '../../StudipIcon.vue';
 import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
 
@@ -70,6 +71,7 @@ const ContainerComponents = {
     CoursewareTypewriterBlock,
     CoursewareVideoBlock,
     //layout
+    CoursewareCompanionBox,
     StudipIcon,
     StudipProgressIndicator,
 };
diff --git a/resources/vue/components/courseware/structural-element/CoursewareBlockadderItem.vue b/resources/vue/components/courseware/structural-element/CoursewareBlockadderItem.vue
deleted file mode 100644
index 921014aae39..00000000000
--- a/resources/vue/components/courseware/structural-element/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/structural-element/CoursewareClipboardItem.vue b/resources/vue/components/courseware/structural-element/CoursewareClipboardItem.vue
deleted file mode 100644
index aa975c8b46c..00000000000
--- a/resources/vue/components/courseware/structural-element/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/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
index ea0d92be4aa..dcb396984dd 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
@@ -209,7 +209,7 @@
                                 <studip-progress-indicator v-if="processing" :description="$gettext('Vorgang wird bearbeitet...')" />
                             </div>
                         </div>
-                        <courseware-toolbar v-if="canVisit && canEdit && editView && !isLink" />
+                        <courseware-toolbar v-if="canVisit && canEdit && editView && !isLink" /> 
                     </div>
                 </div>
                 <studip-dialog
@@ -613,6 +613,7 @@ import CoursewareDateInput from '../layouts/CoursewareDateInput.vue';
 import StockImageSelector from '../../stock-images/SelectorDialog.vue';
 import StudipDialog from '../../StudipDialog.vue';
 import draggable from 'vuedraggable';
+import containerMixin from '@/vue/mixins/courseware/container.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
@@ -638,7 +639,7 @@ export default {
     }),
     props: ['canVisit', 'orderedStructuralElements', 'structuralElement'],
 
-    mixins: [CoursewareExport, CoursewareOerMessage, colorMixin, wizardMixin],
+    mixins: [CoursewareExport, CoursewareOerMessage, colorMixin, wizardMixin, containerMixin],
 
     data() {
         return {
@@ -1239,6 +1240,7 @@ export default {
             showElementRemoveLockDialog: 'showElementRemoveLockDialog',
             updateShowSuggestOerDialog: 'updateShowSuggestOerDialog',
             updateContainer: 'updateContainer',
+            createContainer: 'createContainer',
             sortContainersInStructualElements: 'sortContainersInStructualElements',
             loadTask: 'loadTask',
             loadStructuralElement: 'loadStructuralElement',
diff --git a/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue b/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue
index f7fe3030d69..47b66b19f31 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue
@@ -1,6 +1,7 @@
 <template>
     <div class="cw-blockadder-item-wrapper">
-        <a href="#" @click.prevent="addBlock" class="cw-blockadder-item" :class="['cw-blockadder-item-' + type]">
+        <span class="cw-sortable-handle cw-sortable-handle-blockadder"></span>
+        <a href="#" class="cw-blockadder-item" :class="['cw-blockadder-item-' + type]" @click.prevent="addBlock">
             <header class="cw-blockadder-item-title">
                 {{ title }}
             </header>
@@ -20,10 +21,12 @@
 </template>
 
 <script>
+import containerMixin from '@/vue/mixins/courseware/container.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-blockadder-item',
+    mixins: [containerMixin],
     components: {},
     props: {
         title: String,
@@ -39,8 +42,9 @@ export default {
         ...mapGetters({
             blockAdder: 'blockAdder',
             blockById: 'courseware-blocks/byId',
-            lastCreatedBlock: 'courseware-blocks/lastCreated',
+            containerById: 'courseware-containers/byId',
             favoriteBlockTypes: 'favoriteBlockTypes',
+            lastCreatedBlock: 'courseware-blocks/lastCreated',
         }),
         blockTypeIsFav() {
             return this.favoriteBlockTypes.some((type) => type.type === this.type);
@@ -71,37 +75,16 @@ export default {
             updateContainer: 'updateContainer',
             removeFavoriteBlockType: 'removeFavoriteBlockType',
             addFavoriteBlockType: 'addFavoriteBlockType',
+            setAdderStorage: 'coursewareBlockAdder',
         }),
         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.'),
-                });
-            }
+            this.setAdderStorage({ 
+                container: this.blockAdder.container, 
+                section: this.blockAdder.section, 
+                type: this.type ,
+                position: false
+            });
+            this.addNewBlock();
         },
         toggleFavItem() {
             if (this.blockTypeIsFav) {
diff --git a/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue b/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue
index 27d9e58c5f1..132362628a7 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue
@@ -1,13 +1,14 @@
 <template>
     <div class="cw-clipboard-item-wrapper">
-        <button class="cw-clipboard-item" :class="['cw-clipboard-item-' + kind]" @click.prevent="insertItem">
+        <span class="cw-sortable-handle cw-sortable-handle-clipboard"></span>
+        <button class="cw-clipboard-item" :class="['cw-clipboard-item-' + kind]" @click.prevent="insertClipboardItem">
             <header class="sr-only">
                 {{ srTitle }}
             </header>
             <header class="cw-clipboard-item-title" aria-hidden="true">
                 {{ name }}
             </header>
-            <p class="cw-clipboard-item-description">
+            <p class="cw-clipboard-item-description" :title="description">
                 {{ description }}
             </p>
         </button>
@@ -16,7 +17,7 @@
                 class="cw-clipboard-item-action-menu"
                 :items="menuItems"
                 :context="name"
-                @insertItemCopy="insertItemCopy"
+                @insertItemCopy="insertClipboardItemCopy"
                 @editItem="showEditItem"
                 @deleteItem="deleteItem"
             />
@@ -37,7 +38,7 @@
                 <form class="default" @submit.prevent="">
                     <label>
                         {{ $gettext('Titel') }}
-                        <input type="text" v-model="currentClipboard.attributes.name" />
+                        <input type="text" v-model="currentClipboard.attributes.name" >
                     </label>
                     <label>
                         {{ $gettext('Beschreibung') }}
@@ -50,11 +51,12 @@
 </template>
 
 <script>
+import clipboardMixin from '@/vue/mixins/courseware/clipboard.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-clipboard-item',
-    components: {},
+    mixins: [clipboardMixin],
     props: {
         clipboard: Object,
     },
@@ -62,20 +64,10 @@ export default {
         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() {
@@ -125,9 +117,6 @@ export default {
             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 }) :
@@ -150,80 +139,14 @@ export default {
             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 });
-                }
-            }
+        insertClipboardItem() {
+            this.insertItem(this.clipboard);
         },
 
-        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 });
-                }
-            }
+        insertClipboardItemCopy() {
+            this.insertItemCopy(this.clipboard);
         },
+
         deleteItem() {
             this.deleteClipboard({ id: this.clipboard.id });
         },
diff --git a/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue b/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue
index 78cc4eda3bc..539d8718758 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue
@@ -1,20 +1,33 @@
 <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>
+    <div class="cw-containeradder-item-wrapper">
+        <span class="cw-sortable-handle cw-sortable-handle-containeradder"></span>
+        <a href="#" @click.prevent="addNewContainer">
+            <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>
+        <li class="cw-container-dragitem cw-container-item-sortable" ref="container-drag-item">
+            <div class="cw-container cw-container-list cw-container-item" :class="['cw-container-colspan-' + colspan]">
+                <div class="cw-container-content">
+                    <header class="cw-container-header">{{ title }}</header>
+                    <div class="cw-block-wrapper">{{ description }}</div>
+                </div>
+            </div>
+        </li>
+    </div>
 </template>
 <script>
+import containerMixin from '@/vue/mixins/courseware/container';
 import { mapActions } from 'vuex';
+
 export default {
     name: 'courseware-container-adder-item',
-    components: {},
+    mixins: [containerMixin],
     props: {
         title: String,
         description: String,
@@ -22,31 +35,24 @@ export default {
         colspan: String,
         firstSection: String,
         secondSection: String,
+        newPosition: Number,
     },
     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 = {
+        addNewContainer() {
+            this.addContainer({
+                type: this.type,
                 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.'),
+                sections: {
+                    firstSection: this.firstSection, 
+                    secondSection: this.secondSection
+                },
+                newPosition: null
             });
         },
     },
-    mounted() {},
 };
 </script>
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
index 252f3fe6ce6..300a4e24748 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
@@ -52,9 +52,12 @@
 import CoursewareToolbarBlocks from './CoursewareToolbarBlocks.vue';
 import CoursewareToolbarContainers from './CoursewareToolbarContainers.vue';
 import CoursewareToolbarClipboard from './CoursewareToolbarClipboard.vue';
+import containerMixin from '@/vue/mixins/courseware/container.js';
+import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-toolbar',
+    mixins: [containerMixin],
     components: {
         CoursewareToolbarBlocks,
         CoursewareToolbarContainers,
@@ -73,6 +76,10 @@ export default {
         };
     },
     computed: {
+        ...mapGetters({
+            relatedContainers: 'courseware-containers/related',
+            structuralElementById: 'courseware-structural-elements/byId',
+        }),
         toolbarStyle() {
             const footerHeight = document.getElementById('main-footer').getBoundingClientRect().height;
             const scrollTopStyles = window.getComputedStyle(document.getElementById('scroll-to-top'));
@@ -87,6 +94,12 @@ export default {
                 top: this.toolbarTop + 'px',
             };
         },
+        containers() {
+            return this.relatedContainers({
+                parent: this.structuralElementById({id: this.$route.params.id}), 
+                relationship: 'containers'    
+            });
+        },
         toolbarHeader() {
             let header = '';
             if (this.activeTool === 'blockAdder') {
@@ -142,6 +155,7 @@ export default {
             window.addEventListener('scroll', this.updateToolbarTop);
             window.addEventListener('resize', this.onResize);
         });
+        this.resetAdderStorage();
     },
     beforeDestroy() { 
         window.removeEventListener('scroll', this.updateToolbarTop);
@@ -149,6 +163,11 @@ export default {
     },
 
     watch: {
+        containers(oldValue, newValue) {
+            if (newValue && oldValue.length !== newValue.length) {
+                this.resetAdderStorage();
+            }
+        },
         toolsActive(newState, oldState) {
             let view = this;
             if (newState) {
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
index 3cd783c3fc6..b4081aa2781 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
@@ -10,7 +10,8 @@
                     :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')"
                 />
                 <span class="input-group-append" @click.stop>
-                    <button v-if="searchInput"
+                    <button
+                        v-if="searchInput"
                         type="button"
                         class="button reset-search"
                         id="reset-search"
@@ -19,7 +20,7 @@
                     >
                         <studip-icon shape="decline" :size="20"></studip-icon>
                     </button>
-                    <button 
+                    <button
                         type="submit"
                         class="button"
                         id="search-btn"
@@ -38,8 +39,8 @@
                 v-for="category in blockCategories"
                 :key="category.type"
                 class="button"
-                :class="{'button-active': category.type ===  currentFilterCategory }"
-                :aria-pressed="category.type ===  currentFilterCategory ? 'true' : 'false'"
+                :class="{ 'button-active': category.type === currentFilterCategory }"
+                :aria-pressed="category.type === currentFilterCategory ? 'true' : 'false'"
                 @click="selectCategory(category.type)"
             >
                 {{ category.title }}
@@ -47,14 +48,32 @@
         </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')"
-            />
+            <draggable
+                v-if="filteredBlockTypes.length > 0"
+                class="cw-blockadder-item-list"
+                tag="div"
+                role="listbox"
+                v-model="filteredBlockTypes"
+                handle=".cw-sortable-handle-blockadder"
+                :group="{ name: 'blocks', pull: 'clone', put: 'false' }"
+                :clone="cloneBlock"
+                :sort="false"
+                :emptyInsertThreshold="20"
+                @start="dragBlockStart($event)"
+                @end="dropNewBlock($event)"
+                ref="sortables"
+                sectionId="0"
+            >
+                <courseware-blockadder-item
+                    v-for="(block, index) in filteredBlockTypes"
+                    :key="index"
+                    :title="block.title"
+                    :type="block.type"
+                    :data-blocktype="block.type"
+                    :description="block.description"
+                    @blockAdded="$emit('blockAdded')"
+                />
+            </draggable>
         </div>
         <courseware-companion-box
             v-else
@@ -67,25 +86,31 @@
 <script>
 import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue';
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import containerMixin from '@/vue/mixins/courseware/container.js';
+import draggable from 'vuedraggable';
+
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-toolbar-blocks',
+    mixins: [containerMixin],
     components: {
         CoursewareBlockadderItem,
-        CoursewareCompanionBox
+        CoursewareCompanionBox,
+        draggable,
     },
     data() {
         return {
             searchInput: '',
             currentFilterCategory: '',
             filteredBlockTypes: [],
-            categorizedBlocks: []
+            categorizedBlocks: [],
+
+            isDragging: false,
         };
     },
     computed: {
         ...mapGetters({
-            adderStorage: 'blockAdder',
             unorderedBlockTypes: 'blockTypes',
             favoriteBlockTypes: 'favoriteBlockTypes',
         }),
@@ -104,18 +129,24 @@ export default {
                 { 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' }
+                { title: this.$gettext('Biografie'), type: 'biography' },
             ];
-        }
+        },
     },
     methods: {
         ...mapActions({
-            companionWarning: 'companionWarning'
+            companionWarning: 'companionWarning',
+            createBlock: 'createBlockInContainer',
+            setAdderStorage: 'coursewareBlockAdder',
         }),
         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.')});
+                this.companionWarning({
+                    info: this.$gettext(
+                        'Leider ist Ihr Suchbegriff zu kurz. Der Suchbegriff muss mindestens 3 Zeichen lang sein.'
+                    ),
+                });
                 return;
             }
             this.filteredBlockTypes = this.blockTypes;
@@ -131,33 +162,36 @@ export default {
             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)
-                ));
+            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()];
+                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));
+                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());
+            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) {
+                    if (lowercaseTags.filter((blockTag) => blockTag.includes(tag.toLowerCase())).length > 0) {
                         return true;
                     }
                 }
@@ -183,7 +217,55 @@ export default {
             this.filteredBlockTypes = this.blockTypes;
             this.searchInput = '';
             this.currentFilterCategory = '';
-        }
+        },
+        cloneBlock(original) {
+            original.attributes = {
+                'block-type': original.type,
+                payload: {
+                    file_id: '',
+                    folder_id: '',
+                    background_image_id: '',
+                    files: [],
+                    url: 'studip.de',
+                    sort: 'none',
+                    tool_id: '',
+                    cards: [],
+                    text: ' ',
+                    shapes: {},
+                    type: 'Persönliches Ziel',
+                    content: [{ color: 'blue', label: '', value: '0' }],
+                },
+            };
+            original.relationships = {
+                'user-data-field': {
+                    data: { id: null },
+                },
+            };
+            return original;
+        },
+        dragBlockStart(e) {
+            this.isDragging = true;
+        },
+        async dropNewBlock(e) {
+            const target = e.to.__vue__.$attrs;
+            const blockType = e.item.__vue__.$attrs['data-blocktype'];
+
+            // only execute if dropped in destined list
+            if (!target.containerId) {
+                return;
+            }
+            // set chosen container and section and pass block data
+            this.setAdderStorage({
+                container: this.containerById({ id: target.containerId }),
+                section: target.sectionId,
+                type: blockType,
+                position: e.newIndex,
+            });
+
+            await this.addNewBlock();
+            this.resetAdderStorage();
+            this.isDragging = false;
+        },
     },
     mounted() {
         this.filteredBlockTypes = this.blockTypes;
@@ -206,7 +288,7 @@ export default {
             if (newValue) {
                 this.loadSearch();
             }
-        }
-    }
-}
-</script>
\ No newline at end of file
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
index e7898cd03c1..98cd5730b40 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
@@ -3,12 +3,27 @@
         <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')"
-                    />
+                    <draggable
+                        class="cw-element-inserter-wrapper"
+                        tag="div"
+                        role="listbox"
+                        v-model="clipboardBlocks"
+                        handle=".cw-sortable-handle-clipboard"
+                        :group="{ name: 'blocks', pull: 'clone', put: 'false' }"
+                        :sort="false"
+                        :clone="cloneClipboard"
+                        :emptyInsertThreshold="20"
+                        @end="dropClipboardBlock($event)"
+                        ref="clipboardSortables"
+                        sectionId="0"
+                    >
+                        <courseware-clipboard-item
+                            v-for="(clipboard, index) in clipboardBlocks"
+                            :key="index"
+                            :clipboard="clipboard"
+                            @inserted="$emit('blockAdded')"
+                        />
+                    </draggable>
                 </div>
                 <button class="button trash" @click="clearClipboard('courseware-blocks')">
                     {{ $gettext('Alle Blöcke aus Merkliste entfernen') }}
@@ -23,11 +38,25 @@
         <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"
-                    />
+                    <draggable
+                        class="cw-element-inserter-wrapper"
+                        tag="div"
+                        role="listbox"
+                        v-model="clipboardContainers"
+                        handle=".cw-sortable-handle-clipboard"
+                        :group="{ name: 'description', pull: 'clone', put: 'false' }"
+                        :sort="false"
+                        :emptyInsertThreshold="20"
+                        :clone="cloneClipboardContainer"
+                        @end="dropNewContainer($event)"
+                        ref="clipboardContainerSortables"
+                    >
+                        <courseware-clipboard-item
+                            v-for="(clipboard, index) in clipboardContainers"
+                            :key="index"
+                            :clipboard="clipboard"
+                        />
+                    </draggable>
                 </div>
                 <button class="button trash" @click="clearClipboard('courseware-containers')">
                     {{ $gettext('Alle Abschnitte aus Merkliste entfernen') }}
@@ -48,7 +77,6 @@
             @confirm="executeDeleteClipboard"
             @close="closeDeleteClipboardDialog"
         ></studip-dialog>
-
     </div>
 </template>
 
@@ -56,38 +84,45 @@
 import CoursewareClipboardItem from './CoursewareClipboardItem.vue';
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue';
+import containerMixin from '@/vue/mixins/courseware/container.js';
+import clipboardMixin from '@/vue/mixins/courseware/clipboard.js';
+import draggable from 'vuedraggable';
 import StudipDialog from '../../StudipDialog.vue';
 
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'cw-tools-blockadder',
+    mixins: [containerMixin, clipboardMixin],
     components: {
         CoursewareClipboardItem,
         CoursewareCompanionBox,
         CoursewareCollapsibleBox,
         StudipDialog,
+        draggable,
     },
 
     data() {
         return {
             showDeleteClipboardDialog: false,
-            deleteClipboardType: null
+            deleteClipboardType: null,
+            isDragging: false,
         };
     },
     computed: {
         ...mapGetters({
+            containerById: 'courseware-containers/byId',
             usersClipboards: 'courseware-clipboards/all',
-            userId: 'userId'
+            userId: 'userId',
         }),
         clipboardBlocks() {
             return this.usersClipboards
-                .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-blocks')
+                .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')
+                .filter((clipboard) => clipboard.attributes['object-type'] === 'courseware-containers')
                 .sort((a, b) => b.attributes.mkdate < a.attributes.mkdate);
         },
         textDeleteClipboardTitle() {
@@ -107,12 +142,12 @@ export default {
                 return this.$gettext('Möchten Sie die Merkliste für Abschnitte unwiderruflich leeren?');
             }
             return '';
-        }
+        },
     },
     methods: {
         ...mapActions({
             companionWarning: 'companionWarning',
-            deleteUserClipboards: 'deleteUserClipboards'
+            deleteUserClipboards: 'deleteUserClipboards',
         }),
         clearClipboard(type) {
             this.deleteClipboardType = type;
@@ -120,15 +155,77 @@ export default {
         },
         executeDeleteClipboard() {
             if (this.deleteClipboardType) {
-                this.deleteUserClipboards({uid: this.userId, type: this.deleteClipboardType});
+                this.deleteUserClipboards({ uid: this.userId, type: this.deleteClipboardType });
             }
             this.closeDeleteClipboardDialog();
         },
         closeDeleteClipboardDialog() {
             this.showDeleteClipboardDialog = false;
             this.deleteClipboardType = null;
-        }
-    }
-};
+        },
+        cloneClipboard(original) {
+            original.attributes['block-type'] = original.attributes['object-kind'];
+            original.attributes.payload = {};
+            original.relationships = {
+                'user-data-field': {
+                    data: { id: null },
+                },
+                block: {},
+            };
+            return original;
+        },
+        async dropClipboardBlock(e) {
+            const target = e.to.__vue__.$attrs;
+            // only execute if dropped in destined list
+            if (!target.containerId) {
+                return;
+            }
+            // set chosen container and section and insert the clipboard block
+            this.setAdderStorage({
+                container: this.containerById({ id: target.containerId }),
+                section: target.sectionId,
+                position: e.newIndex,
+            });
+            await this.insertItem(e.item.__vue__._data.currentClipboard);
+            this.resetAdderStorage();
+        },
+        cloneClipboardContainer(original) {
+            original.newContainer = true;
+            original.clipContainer = true;
+            original.attributes['container-type'] = original.attributes['object-kind'];
+            original.type = 'courseware-containers';
+            original.attributes.payload = {};
+            original.relationships = {};
+            original.relationships.container = {};
+            original.relationships.blocks = {};
+            original.relationships.blocks.data = {};
+            return original;
+        },
+        dropNewContainer(e) {
+            // if the container is dropped back to its original list, do nothing / cancel the operation
+            if (e.to.className === 'cw-containeradder-item-list' || e.to.className === 'cw-element-inserter-wrapper') {
+                this.isDragging = false;
+                return;
+            }
 
-</script>
\ No newline at end of file
+            const item = e.item._underlying_vm_;
+
+            // if the container is from the clipboard, insert it via clipboard mixin, else add it via container mixin
+            if (item.clipContainer) {
+                this.insertItem(e.item.__vue__._data.currentClipboard, e.newIndex);
+            } else {
+                const data = {
+                    type: item.attributes['container-type'],
+                    colspan: item.containerStyle,
+                    sections: {
+                        firstSection: item.firstSection,
+                        secondSection: item.secondSection,
+                    },
+                    newPosition: e.newIndex,
+                };
+                this.addContainer(data);
+            }
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue
index 12fea1bbd05..c5781b24e49 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue
@@ -1,66 +1,160 @@
 <template>
     <div class="cw-toolbar-containers">
         <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>
+            <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>
+        <draggable
+            class="cw-containeradder-item-list"
+            tag="div"
+            role="listbox"
+            v-model="containerTypes"
+            handle=".cw-sortable-handle-containeradder"
+            :group="{ name: 'description', pull: 'clone', put: 'false' }"
+            :clone="cloneContainer"
+            :sort="false"
+            :emptyInsertThreshold="20"
+            @start="dragContainerStart($event)"
+            @end="dropNewContainer($event)"
+            ref="containerSortables"
+        >
+            <courseware-container-adder-item
+                v-for="container in containerTypes"
+                :key="container.type"
+                :title="container.title"
+                :type="container.type"
+                :colspan="selectedContainerStyle"
+                :description="container.description"
+                :firstSection="firstSection"
+                :secondSection="secondSection"
+                :newPosition="newContainerPosition"
+            ></courseware-container-adder-item>
+        </draggable>
     </div>
 </template>
 
 <script>
 import CoursewareContainerAdderItem from './CoursewareContainerAdderItem.vue';
+import containerMixin from '@/vue/mixins/courseware/container.js';
+import draggable from 'vuedraggable';
 import { mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-toolbar-containers',
+    mixins: [containerMixin],
     components: {
-        CoursewareContainerAdderItem
+        CoursewareContainerAdderItem,
+        draggable,
     },
     data() {
         return {
-            selectedContainerStyle: 'full'
+            selectedContainerStyle: 'full',
+            isDragging: false,
         };
     },
     computed: {
         ...mapGetters({
-            containerTypes: 'containerTypes'
+            containerTypes: 'containerTypes',
+            structuralElementById: 'courseware-structural-elements/byId',
+            relatedContainers: 'courseware-containers/related',
+
         }),
         containerStyles() {
             return [
-                { key: 0, title: this.$gettext('Volle Breite'), colspan: 'full'},
+                { 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' }
+                { key: 2, title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' },
             ];
         },
+        containers() {
+            return this.relatedContainers({
+                parent: this.structuralElementById({id: this.$route.params.id}), 
+                relationship: 'containers'    
+            });
+        },
+        newContainerPosition() {
+            return this.containers?.length || 0;
+        },
+        firstSection() {
+            return this.$gettext('erstes Element');
+        },
+        secondSection() {
+            return this.$gettext('zweites Element');
+        },
+    },
+    methods: {
+        cloneContainer(original) {
+            original.newContainer = true;
+            original.attributes = {};
+            original.attributes['container-type'] = original.type;
+            original.attributes.payload = {};
+            original.relationships = {};
+            original.relationships.blocks = {};
+            original.relationships.blocks.data = {};
+            original.firstSection = this.firstSection;
+            original.secondSection = this.secondSection;
+            original.containerStyle = this.selectedContainerStyle;
+            return original;
+        },
+        cloneClipboardContainer(original) {
+            original.newContainer = true;
+            original.clipContainer = true;
+            original.attributes['container-type'] = original.attributes['object-kind'];
+            original.type = 'courseware-containers';
+            original.attributes.payload = {};
+            original.relationships = {};
+            original.relationships.container = {};
+            original.relationships.blocks = {};
+            original.relationships.blocks.data = {};
+            return original;
+        },
+        dragContainerStart(e) {
+            this.isDragging = true;
+        },
+        dropNewContainer(e) {
+            // if the container is dropped back to its original list, do nothing / cancel the operation
+            if (e.to.className === 'cw-containeradder-item-list' || e.to.className === 'cw-element-inserter-wrapper') {
+                this.isDragging = false;
+                return;
+            }
+
+            const item = e.item._underlying_vm_;
+
+            // if the container is from the clipboard, insert it via clipboard mixin, else add it via container mixin
+            if (item.clipContainer) {
+                this.insertItem(e.item.__vue__._data.currentClipboard, e.newIndex);
+            } else {
+                const data = {
+                type: item.attributes['container-type'],
+                colspan: item.containerStyle,
+                sections: {
+                        firstSection: item.firstSection,
+                        secondSection: item.secondSection
+                    },
+                newPosition: e.newIndex
+                };
+                this.addContainer(data);
+            }
+        },
     }
-}
-</script>
\ No newline at end of file
+};
+</script>
diff --git a/resources/vue/mixins/courseware/clipboard.js b/resources/vue/mixins/courseware/clipboard.js
new file mode 100644
index 00000000000..8ae4170dbdb
--- /dev/null
+++ b/resources/vue/mixins/courseware/clipboard.js
@@ -0,0 +1,125 @@
+import containerMixin from './container';
+import { mapActions, mapGetters } from 'vuex';
+
+const clipboardMixin = {
+    mixins: [containerMixin],
+    data() {
+        return {
+            text: {
+                errorMessage: this.$gettext('Es ist ein Fehler aufgetreten.'),
+                positionWarning: this.$gettext(
+                    'Bitte fügen Sie der Seite einen Abschnitt hinzu, damit der Block eingefügt werden kann.'
+                ),
+                blockSuccess: this.$gettext('Der Block wurde erfolgreich eingefügt.'),
+                containerSuccess: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'),
+            }
+        }
+    },
+    computed: {
+        ...mapGetters({
+            blockAdder: 'blockAdder',
+            currentElement: 'currentElement',
+        }),
+
+        blockAdderActive() {
+            return Object.keys(this.blockAdder).length !== 0;
+        },
+    },
+    methods: {
+        ...mapActions({
+            companionInfo: 'companionInfo',
+            companionSuccess: 'companionSuccess',
+            companionWarning: 'companionWarning',
+            clipboardInsertBlock: 'clipboardInsertBlock',
+            clipboardInsertContainer: 'clipboardInsertContainer',
+            loadStructuralElement: 'loadStructuralElement',
+            loadContainer: 'loadContainer',
+            loadClipboard: 'courseware-clipboards/loadById',
+        }),
+
+        async insertItem(clipboard, itemPosition) {
+            const isBlock = clipboard.attributes['object-type'] === 'courseware-blocks';
+            let insertError = false;
+
+            if (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: clipboard,
+                    });
+                } catch (error) {
+                    insertError = true;
+                    this.companionWarning({ info: this.text.errorMessage });
+                }
+                if (!insertError) {
+                    await this.loadContainer(this.blockAdder.container.id);
+                    if (this.blockAdder.position !== undefined) {
+                        await this.sortClipboardBlock();
+                    }
+                    this.companionSuccess({ info: this.text.blockSuccess });
+                }
+            } else {
+                try {
+                    await this.clipboardInsertContainer({
+                        parentId: this.currentElement,
+                        clipboard: clipboard,
+                    });
+
+                } catch (error) {
+                    insertError = true;
+                    this.companionWarning({ info: this.text.errorMessage });
+                }
+                if (!insertError) {
+                    await this.loadStructuralElement(this.currentElement);
+                    itemPosition = itemPosition ? itemPosition : 'last';
+                    this.sortContainer(itemPosition);
+                    this.companionSuccess({ info: this.text.containerSuccess });
+                }
+            }
+        },
+
+        async insertItemCopy(clipboard) {
+            const isBlock = clipboard.attributes['object-type'] === 'courseware-blocks';
+            let insertError = false;
+
+            if (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 });
+                }
+            }
+        },
+    }
+}
+
+export default clipboardMixin;
\ No newline at end of file
diff --git a/resources/vue/mixins/courseware/container.js b/resources/vue/mixins/courseware/container.js
index e4e02ddef53..db60af2f30b 100644
--- a/resources/vue/mixins/courseware/container.js
+++ b/resources/vue/mixins/courseware/container.js
@@ -3,9 +3,14 @@ import { mapActions, mapGetters } from 'vuex';
 const containerMixin = {
     computed: {
         ...mapGetters({
+            blockAdder: 'blockAdder',
             blockById: 'courseware-blocks/byId',
             containerById: 'courseware-containers/byId',
             pluginManager: 'pluginManager',
+            lastCreatedBlock: 'courseware-blocks/lastCreated',
+            lastCreatedContainers: 'courseware-containers/lastCreated',
+            blockedByAnotherUser: 'currentElementBlockedByAnotherUser',
+            currentStructuralElement: 'currentStructuralElement',
         }),
     },
     created: function () {
@@ -16,8 +21,16 @@ const containerMixin = {
             updateBlock: 'updateBlock',
             updateContainer: 'updateContainer',
             loadContainer: 'courseware-containers/loadById',
+            loadStructuralElement: 'loadStructuralElement',
             lockObject: 'lockObject',
             unlockObject: 'unlockObject',
+            createBlock: 'createBlockInContainer',
+            createContainer: 'createContainer',
+            companionInfo: 'companionInfo',
+            companionSuccess: 'companionSuccess',
+            companionWarning: 'companionWarning',
+            sortContainersInStructualElements: 'sortContainersInStructualElements',
+            setAdderStorage: 'coursewareBlockAdder',
         }),
         dropBlock(e) {
             this.isDragging = false; // implemented bei echt container type
@@ -51,14 +64,16 @@ const containerMixin = {
             await this.unlockObject({ id: data.blockId, type: 'courseware-blocks' });
 
             // update origin container
-            let originContainer = this.containerById({ id: data.originContainerId});
-            originContainer.attributes.payload.sections[data.originSectionId].blocks = data.originSectionBlockList;
-            await this.lockObject({ id: data.originContainerId, type: 'courseware-containers' });
-            await this.updateContainer({
-                container: originContainer,
-                structuralElementId: originContainer.relationships['structural-element'].data.id,
-            });
-            await this.unlockObject({ id: data.originContainerId, type: 'courseware-containers' });
+            if (data.originContainerId) {
+                let originContainer = this.containerById({ id: data.originContainerId});
+                originContainer.attributes.payload.sections[data.originSectionId].blocks = data.originSectionBlockList;
+                await this.lockObject({ id: data.originContainerId, type: 'courseware-containers' });
+                await this.updateContainer({
+                    container: originContainer,
+                    structuralElementId: originContainer.relationships['structural-element'].data.id,
+                });
+                await this.unlockObject({ id: data.originContainerId, type: 'courseware-containers' });
+            }
 
             // update target container
             let targetContainer = this.containerById({ id: data.targetContainerId});
@@ -77,7 +92,171 @@ const containerMixin = {
             return Array.isArray(firstSet) && Array.isArray(secondSet) &&
                 firstSet.length === secondSet.length &&
                 firstSet.every((val, index) => val === secondSet[index]);
-        }
+        },
+        async addNewBlock() {
+            if (this.blockAdder.container) {
+                const targetContainer = this.blockAdder.container;
+                const section = this.blockAdder.section;
+                const type = this.blockAdder.type;
+                const position = this.blockAdder.position;
+
+                try {
+                    await this.lockObject({ id: targetContainer.id, type: 'courseware-containers' });
+                } catch (error) {
+                    if (error.status === 409) {
+                        this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+                    } else {
+                        console.log(error);
+                    }
+                }
+                
+                await this.createBlock({
+                    container: targetContainer,
+                    section: section,
+                    blockType: type,
+                });
+                // get the just created block to add it to a container and adjust its position if applicable
+                const newBlock = this.lastCreatedBlock;
+
+                // if the block is dropped to a specific position, save it at the correct position
+                if (position !== false) {
+                    targetContainer.attributes.payload.sections[section].blocks.splice(
+                        position, 0, newBlock.id);
+                // otherwise put it in the last position of the last container
+                } else {
+                    targetContainer.attributes.payload.sections[section].blocks.push(newBlock.id);
+                }
+
+                const structuralElementId = targetContainer.relationships['structural-element'].data.id;
+
+                await this.updateContainer({ container: targetContainer, structuralElementId: structuralElementId });
+                await this.unlockObject({ id: targetContainer.id, type: 'courseware-containers' });
+                this.companionSuccess({
+                    info: this.$gettext('Der Block wurde erfolgreich eingefügt.'),
+                });
+            } else {
+                // companion action
+                this.companionWarning({
+                    info: this.$gettext('Bitte fügen Sie der Seite einen Abschnitt hinzu, damit der Block eingefügt werden kann.'),
+                });
+            }
+        },
+        async sortClipboardBlock() {
+            const targetContainer = this.blockAdder.container;
+            const position = this.blockAdder.position;
+
+            try {
+                await this.lockObject({ id: targetContainer.id, type: 'courseware-containers' });
+            } catch (error) {
+                if (error.status === 409) {
+                    this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+                } else {
+                    console.log(error);
+                }
+            }
+            const containerBlocks = targetContainer.attributes.payload.sections[this.blockAdder.section].blocks;
+            containerBlocks.splice(position, 0, containerBlocks.pop());
+
+            const structuralElementId = targetContainer.relationships['structural-element'].data.id;
+            try {
+                await this.updateContainer({ container: targetContainer, structuralElementId: structuralElementId });
+                await this.unlockObject({ id: targetContainer.id, type: 'courseware-containers' });
+            } catch (error) {
+                this.companionWarning({
+                    info: this.$gettext('Der Block konnte nicht hinzugefügt werden, bitte versuchen Sie es erneut.'),
+                });
+                console.log(error);
+            }
+        },
+        async addContainer(data) {
+            const type = data.type;
+            const colspan = data.colspan;
+            const firstSection = data.sections.firstSection;
+            const secondSection = data.sections.secondSection;
+
+            let attributes = {};
+            attributes["container-type"] = type;
+            let sections = [];
+            if (type === 'list') {
+                sections = [{ name: firstSection, icon: '', blocks: [] }];
+            } else {
+                sections = [{ name: firstSection, icon: '', blocks: [] },{ name: secondSection, icon: '', blocks: [] }];
+            }
+            attributes.payload = {
+                colspan: colspan,
+                sections: sections,
+            };
+            await this.createContainer({ structuralElementId: this.$route.params.id, attributes: attributes });
+            this.companionSuccess({
+                info: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'),
+            });
+
+            // if the container was dropped to a specific position, sort it and update the structural element
+            if (data.newPosition != null) {
+                this.sortContainer(data.newPosition);
+            }
+        },
+        async sortContainer(newContainerPos) {
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+                return false;
+            }
+            try {
+                await this.lockObject({ id: this.currentStructuralElement.id, 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;
+            }
+            // insert the newly created container at the correct position
+            let containerList = [];
+            this.currentStructuralElement.relationships.containers.data.forEach(container => {
+                containerList.push(container);
+            });
+
+            if (newContainerPos != null) {
+                // find the container with the highest index (= latest addition) because it isn't
+                // added at the bottom when it is a clipboard
+                const highestIndexContainer = containerList.reduce((previous, current) => {
+                    return (previous && parseInt(previous.id) > parseInt(current.id)) ? previous : current;
+                }, 0);
+
+                // get the last created container if a new container is added, or
+                // the highest index container in the case of a clipboard
+                const newestContainer = this.lastCreatedContainers?.id || highestIndexContainer.id;
+                const tempPosition = containerList.findIndex(x => x.id === newestContainer);
+                const newContainer = containerList.splice(tempPosition, 1)[0];
+
+                if (newContainerPos === 'last') {
+                    newContainerPos = containerList.length;
+                }
+                containerList.splice(newContainerPos, 0, newContainer);
+            }
+            await this.sortContainersInStructualElements({
+                structuralElement: this.currentStructuralElement,
+                containers: containerList,
+            });
+            await this.loadStructuralElement(this.currentStructuralElement.id);
+
+            this.$emit('select', this.currentStructuralElement.id);
+
+            return false;
+        },
+        resetAdderStorage() {
+            // choose the last container and its last section as the default adder slot
+            // for adding blocks and containers via click
+            if (this.containers) {
+                this.setAdderStorage({
+                    container: this.containers[this.containers.length - 1],
+                    section: this.containers[this.containers.length - 1].attributes.payload.sections.length - 1
+                });
+            }
+        },
+
     }
 };
 
-- 
GitLab