diff --git a/app/views/questionnaire/question_types/likert/likert_answer.php b/app/views/questionnaire/question_types/likert/likert_answer.php
index 18475877214d16c8bae78cf4e787aa3592ddeef3..86ccc3eb0c64f5e549f563a931b1d777feba0bb9 100644
--- a/app/views/questionnaire/question_types/likert/likert_answer.php
+++ b/app/views/questionnaire/question_types/likert/likert_answer.php
@@ -25,7 +25,7 @@ $responseData = isset($response->answerdata['answers']) ? $response->answerdata[
             <tr>
                 <th><?= _('Aussage') ?></th>
                 <? foreach ($answers as $answer) : ?>
-                <th><?= htmlReady($answer) ?></th>
+                <th class="option-cell"><?= htmlReady($answer) ?></th>
                 <? endforeach ?>
             </tr>
         </thead>
@@ -35,7 +35,7 @@ $responseData = isset($response->answerdata['answers']) ? $response->answerdata[
                 <? $html_id = md5(uniqid($index)) ?>
                 <td id="<?= $html_id ?>"><?= htmlReady($statements[$index]) ?></td>
                 <? foreach ($answers as $answer_index => $answer) : ?>
-                    <td>
+                    <td class="option-cell">
                         <input type="radio"
                                title="<?= htmlReady($answer) ?>"
                                aria-labelledby="<?= $html_id ?>"
diff --git a/app/views/questionnaire/question_types/likert/likert_evaluation.php b/app/views/questionnaire/question_types/likert/likert_evaluation.php
index fd4eb019eca338adb4b0ff2b0073b16974195797..11ef4b4b98fdfb71c726c566e1db9616976a8e79 100644
--- a/app/views/questionnaire/question_types/likert/likert_evaluation.php
+++ b/app/views/questionnaire/question_types/likert/likert_evaluation.php
@@ -27,7 +27,7 @@ $options = $vote->questiondata['options'];
         <tr>
             <th><?= _('Aussage') ?></th>
             <? foreach ($options as $option) : ?>
-                <th><?= htmlReady($option) ?></th>
+                <th class="option-cell"><?= htmlReady($option) ?></th>
             <? endforeach ?>
         </tr>
     </thead>
diff --git a/app/views/questionnaire/question_types/rangescale/rangescale_answer.php b/app/views/questionnaire/question_types/rangescale/rangescale_answer.php
index 3525b4cff4856742b4e966f85a4e5bea5e5255d1..43336891f84bfdc97d1c9046e1eba86f3d19b5d6 100644
--- a/app/views/questionnaire/question_types/rangescale/rangescale_answer.php
+++ b/app/views/questionnaire/question_types/rangescale/rangescale_answer.php
@@ -25,7 +25,7 @@ $responseData = $response['answerdata'] && $response['answerdata']['answers'] ?
             <tr>
                 <th><?= _('Aussage') ?></th>
                 <? for ($i = $vote->questiondata['minimum'] ?? 1; $i <= $vote->questiondata['maximum']; $i++) : ?>
-                <th><?= htmlReady($i) ?></th>
+                <th class="option-cell"><?= htmlReady($i) ?></th>
                 <? endfor ?>
             </tr>
         </thead>
@@ -35,7 +35,7 @@ $responseData = $response['answerdata'] && $response['answerdata']['answers'] ?
                 <? $html_id = md5(uniqid($index)) ?>
                 <td id="<?= $html_id ?>"><?= htmlReady($statements[$index]) ?></td>
                 <? for ($i = $vote->questiondata['minimum'] ?? 1; $i <= $vote->questiondata['maximum']; $i++) : ?>
-                    <td>
+                    <td class="option-cell">
                         <input type="radio"
                                title="<?= htmlReady($i) ?>"
                                aria-labelledby="<?= $html_id ?>"
diff --git a/app/views/questionnaire/question_types/rangescale/rangescale_evaluation.php b/app/views/questionnaire/question_types/rangescale/rangescale_evaluation.php
index 2715934beeadc769310a05133564c9a350168ca1..6b75ea1abe4a1fab0eebfe5fe6a155fbb397346f 100644
--- a/app/views/questionnaire/question_types/rangescale/rangescale_evaluation.php
+++ b/app/views/questionnaire/question_types/rangescale/rangescale_evaluation.php
@@ -27,7 +27,7 @@ $options = range($vote->questiondata['minimum'], $vote->questiondata['maximum'])
         <tr>
             <th><?= _('Aussage') ?></th>
             <? for ($i = $vote->questiondata['minimum'] ?? 1; $i <= $vote->questiondata['maximum']; $i++) : ?>
-                <th class="rangescale_center"><?= htmlReady($i) ?></th>
+                <th class="option-cell"><?= htmlReady($i) ?></th>
             <? endfor ?>
         </tr>
     </thead>
diff --git a/resources/assets/stylesheets/scss/questionnaire.scss b/resources/assets/stylesheets/scss/questionnaire.scss
index f55b50f36b0d76ae3066cb7593026cbb8ea104b9..fde7d32a2bf1c40ae29adb115cd941046ffec05f 100644
--- a/resources/assets/stylesheets/scss/questionnaire.scss
+++ b/resources/assets/stylesheets/scss/questionnaire.scss
@@ -1,8 +1,6 @@
 $width: 270px;
 
 .questionnaire_edit {
-
-
     .editor {
         display: flex;
         flex-direction: row-reverse;
@@ -14,14 +12,16 @@ $width: 270px;
             min-width: $width;
             width: $width;
             .questions_container {
-                padding: 0px;
+                padding: 0;
                 .questions {
                     display: flex;
                     flex-direction: column;
                 }
             }
 
-            > .admin, > .add_question, .questions > * {
+            > .admin,
+            > .add_question,
+            .questions > * {
                 width: calc(100% - 8px);
                 padding: 4px;
                 border-bottom: 1px solid var(--content-color-40);
@@ -42,8 +42,8 @@ $width: 270px;
                     &::before {
                         content: '';
                         position: absolute;
-                        height: 0px;
-                        width: 0px;
+                        height: 0;
+                        width: 0;
                         border-top: 25px transparent solid;
                         border-bottom: 25px transparent solid;
                         border-left: 7px var(--content-color-40) solid;
@@ -52,8 +52,8 @@ $width: 270px;
                     &::after {
                         content: '';
                         position: absolute;
-                        height: 0px;
-                        width: 0px;
+                        height: 0;
+                        width: 0;
                         border-top: 25px transparent solid;
                         border-bottom: 25px transparent solid;
                         border-left: 7px var(--yellow-40) solid;
@@ -93,42 +93,11 @@ $width: 270px;
             border: 1px solid var(--content-color-40);
             border-left: none;
             flex-grow: 1;
-            padding: 10px;
-            padding-left: 15px;
+            padding: 10px 10px 10px 15px;
             min-height: 150px;
             min-width: 0;
         }
-        .vote_edit {
-            .options {
-                > li {
-                    display: flex;
-                    align-items: center;
-                    > * {
-                        margin-right: 10px;
-                    }
-                }
-            }
-        }
-        .rangescale_edit table.default > thead > tr > th.number {
-            padding-left: 12px;
-        }
-
-        .dragcolumn {
-            max-width: 1px;
-            padding-bottom: 0px;
-            > .dragarea {
-                display: inline-block;
-                height: 27px;
-            }
-        }
-
-        .input-array {
-            margin-left: 4px;
-        }
 
-        .likert_edit .input-array {
-            margin-left: 7px;
-        }
         .inline_editing {
             width: 100%;
             display: flex;
@@ -150,114 +119,25 @@ $width: 270px;
                 justify-items: center;
             }
         }
-        .drag-handle {
-            display: inline-block;
-            height: 24px;
-        }
-    }
-
-    /* ab hier der alte kram */
-
-    section {
-        border: thin solid var(--black);
-        margin: 3px;
-    }
-
-    .options {
-        padding: 0;
-        list-style-type: none;
-
-        > li {
-            margin-top: 5px;
-            margin-bottom: 5px;
-
-            > .move {
-                cursor: move;
-                display: inline-block;
-                vertical-align: middle;
-            }
-
-            > input {
-                display: inline-block;
-                vertical-align: middle;
-            }
-
-            > input[type=text] {
-                width: calc(100% - 70px);
-            }
 
-            .delete {
+        .dragcolumn {
+            max-width: 1px;
+            padding-bottom: 0;
+            > .dragarea {
                 display: inline-block;
-                vertical-align: middle;
-                cursor: pointer;
-            }
-
-            .add {
-                display: none;
-                vertical-align: middle;
-                cursor: pointer;
+                height: 27px;
             }
         }
 
-        > li:last-child .delete {
-            display: none;
-        }
-
-        > li:last-child .add {
+        .drag-handle {
             display: inline-block;
+            height: 24px;
         }
 
-        > li:only-child .move {
-            display: none;
-        }
-
-    }
-
-    .all_questions {
-        .question:first-child .move_up {
-            display: none;
-        }
-
-        .question:last-child .move_down {
-            display: none;
-        }
-    }
-
-    .add_questions {
-        display: flex;
-        flex-wrap: wrap;
-        justify-content: center;
-        align-items: stretch;
-        border: thin dashed var(--content-color-40);
-
-        > a {
-            background-color: transparent;
-            margin: 10px;
-            border: thin solid var(--content-color-20);
-            padding: 5px;
-            width: 100px;
-            min-width: 100px;
-            max-width: 100px;
-            height: 100px;
-            min-height: 100px;
-            max-height: 100px;
-            overflow: hidden;
-            display: flex;
-            flex-direction: column;
-            justify-content: space-around;
-            align-items: center;
+        .option-cell {
             text-align: center;
-
-            > img {
-                margin-left: auto;
-                margin-right: auto;
-            }
         }
     }
-
-    .questionnaire_metadata {
-        margin-top: 10px;
-    }
 }
 
 .questionnaire_results {
@@ -308,7 +188,8 @@ $width: 270px;
 }
 
 
-.questionnaire_answer, .questionnaire_results {
+.questionnaire_answer,
+.questionnaire_results {
     .description_container {
         display: flex;
         > .icon_container {
@@ -335,7 +216,7 @@ $width: 270px;
         border: none;
 
         > :first-child {
-            margin-top: 0px;
+            margin-top: 0;
         }
 
         .invalidation_notice {
@@ -351,9 +232,6 @@ $width: 270px;
         font-size: 0.7em;
         padding-left: 5px;
     }
-    .rangescale_center {
-        text-align: center;
-    }
     .centerline {
         border-top: 1px solid var(--base-color);
         position: relative;
@@ -389,6 +267,14 @@ $width: 270px;
     }
 }
 
+.questionnaire_edit,
+.questionnaire_answer,
+.questionnaire_results {
+    .option-cell {
+        text-align: center;
+    }
+}
+
 .courseselector,
 .instituteselector,
 .statusgroupselector {
diff --git a/resources/vue/components/questionnaires/FreetextEdit.vue b/resources/vue/components/questionnaires/FreetextEdit.vue
index 29c6f34b81781a832e11a7238ff41db6bc6a5f21..58848edd1d51e58c718734c1b24c41b7d921c06e 100644
--- a/resources/vue/components/questionnaires/FreetextEdit.vue
+++ b/resources/vue/components/questionnaires/FreetextEdit.vue
@@ -2,7 +2,7 @@
     <div>
         <div class="formpart" tabindex="0" ref="autofocus">
             {{ $gettext('Frage') }}
-            <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+            <StudipWysiwyg v-model="val_clone.description" />
         </div>
 
         <label>
@@ -13,40 +13,19 @@
 </template>
 
 <script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
 
 export default {
     name: 'freetext-edit',
-    components: {
-        StudipWysiwyg
+    mixins: [ QuestionnaireComponent ],
+    created() {
+        this.setDefaultValues({
+            description: '',
+            mandatory: '0',
+        });
     },
-    props: {
-        value: {
-            type: Object,
-            required: false,
-            default: function () {
-                return {};
-            }
-        },
-        question_id: {
-            type: String,
-            required: false
-        }
-    },
-    data: function () {
-        return {
-            val_clone: ''
-        };
-    },
-    mounted: function () {
-        this.val_clone = this.value;
+    mounted() {
         this.$refs.autofocus.focus();
-    },
-    watch: {
-        value (new_val) {
-            this.val_clone = new_val;
-        }
     }
-
 }
 </script>
diff --git a/resources/vue/components/questionnaires/InputArray.vue b/resources/vue/components/questionnaires/InputArray.vue
index 0fd67db96dbdd8619d8e73918b9f26459ff1ebc8..f57ca58d33acba08253f20d2c611859197e5ef33 100644
--- a/resources/vue/components/questionnaires/InputArray.vue
+++ b/resources/vue/components/questionnaires/InputArray.vue
@@ -1,206 +1,170 @@
 <template>
     <div class="input-array">
         <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
-        <draggable v-model="options" handle=".dragarea" tag="ol" class="clean options">
-            <li v-for="(option, index) in options" :key="index">
-                <a class="dragarea"
-                   v-if="options.length > 1"
-                   tabindex="0"
-                   :ref="'draghandle_' + index"
-                   :title="$gettextInterpolate('Sortierelement für Option %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {option: option})"
-                   @keydown="keyHandler($event, index)">
-                    <span class="drag-handle"></span>
-                </a>
-                <input type="text"
-                       :placeholder="$gettext('Option')"
-                       :ref="'option_' + index"
-                       @paste="(ev) => onPaste(ev, index)"
-                       v-model="options[index]">
-                <button class="as-link"
-                   :title="$gettext('Option löschen')"
-                   @click.prevent="askForDeletingOption(index)">
-                    <studip-icon shape="trash" :role="options.length > 1 ? 'clickable' : 'inactive'" :size="20" alt=""></studip-icon>
-                </button>
-            </li>
-        </draggable>
 
-        <button class="as-link"
-                :title="$gettext('Option hinzufügen')"
-                @click.prevent="addOption">
-            <studip-icon shape="add" :size="20" alt=""></studip-icon>
-        </button>
-
-        <studip-dialog
-            v-if="askForDeleting"
-            :title="$gettext('Bitte bestätigen Sie die Aktion.')"
-            :question="$gettext('Wirklich löschen?')"
-            :confirmText="$gettext('Ja')"
-            :closeText="$gettext('Nein')"
-            closeClass="cancel"
-            height="180"
-            @confirm="deleteOption"
-            @close="askForDeleting = false"
-        >
-        </studip-dialog>
+        <table class="default nohover">
+            <colgroup>
+                <col style="width: 16px">
+                <col>
+                <col v-for="i in additionalColspan" :key="`colspan-${i}`">
+                <col style="width: 24px">
+            </colgroup>
+            <thead>
+                <tr>
+                    <th class="dragcolumn"></th>
+                    <th>{{ labelPlural }}</th>
+                    <slot name="header-cells" />
+                    <th class="actions"></th>
+                </tr>
+            </thead>
+            <Draggable v-model="options" handle=".dragarea" tag="tbody" class="statements">
+                <tr v-for="(option, index) in options" :key="index">
+                    <td class="dragcolumn">
+                        <a class="dragarea"
+                           tabindex="0"
+                           :title="$gettextInterpolate($gettext(`Sortierelement für %{label} %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.`), {option, label})"
+                           @keydown="keyHandler($event, index)"
+                           ref="draghandle">
+                            <span class="drag-handle"></span>
+                        </a>
+                    </td>
+                    <td>
+                        <input type="text"
+                               ref="inputs"
+                               :placeholder="label"
+                               @paste="(ev) => onPaste(ev, index)"
+                               v-model="options[index]">
+                    </td>
+                    <slot name="body-cells" />
+                    <td class="actions">
+                        <StudipIcon name="delete"
+                                     shape="trash"
+                                     :size="20"
+                                     @click.prevent="deleteOption(index)"
+                                     :title="$gettextInterpolate($gettext('%{label} löschen'), {label})"
+                        />
+                    </td>
+                </tr>
+            </Draggable>
+            <tfoot>
+                <tr>
+                    <td :colspan="3 + additionalColspan">
+                        <button class="as-link"
+                                :title="$gettextInterpolate($gettext('%{label} hinzufügen'),  {label})"
+                                @click.prevent="addOption()">
+                            <StudipIcon shape="add" :size="20" alt="" />
+                        </button>
+                    </td>
+                </tr>
+            </tfoot>
+        </table>
     </div>
 </template>
 
 <script>
-import StudipIcon from "../StudipIcon.vue";
-import StudipDialog from "../StudipDialog.vue";
-import draggable from 'vuedraggable';
+import Draggable from 'vuedraggable';
+import { $gettext } from '../../../assets/javascripts/lib/gettext';
+
 export default {
     name: 'input-array',
-    components: {
-        StudipIcon,
-        StudipDialog,
-        draggable
-    },
+    components: { Draggable },
     props: {
-        value: {
-            type: Array,
-            required: false
-        }
+        additionalColspan: {
+            type: Number,
+            default: 0,
+        },
+        label: {
+            type: String,
+            default: $gettext('Option'),
+        },
+        labelPlural: {
+            type: String,
+            default: $gettext('Optionen'),
+        },
+        value: Array,
     },
-    data: function () {
+    data() {
         return {
             options: [],
-            askForDeleting: false,
-            indexOfDeletingOption: 0,
-            unique_id: null,
-            assistiveLive: ''
+            assistiveLive: '',
         };
     },
     methods: {
-        addOption: function (val, position) {
-            let data = this.value;
-            if (val.target) {
-                val = '';
-            }
-            if (typeof position === "undefined") {
-                data.push(val || '');
-                position = this.value.length - 1
-            } else {
-                data.splice(position, 0, val || '');
-            }
-            this.$emit('input', data);
-            let v = this;
-            this.$nextTick(function () {
-                v.$refs['option_' + position][0].focus();
-            });
-        },
-        askForDeletingOption: function (index) {
-            if (this.options.length <= 1) {
-                return;
-            }
+        addOption(val = '', position = this.options.length) {
+            this.$set(this.options, position, val.trim());
 
-            this.indexOfDeletingOption = index;
-            if (this.value[index]) {
-                this.askForDeleting = true;
-            } else {
-                this.deleteOption();
-            }
+            this.$nextTick(() => {
+                this.$refs.inputs[position].focus();
+            });
         },
-        deleteOption: function () {
-            this.$delete(this.value, this.indexOfDeletingOption);
-            this.askForDeleting = false;
+        deleteOption(index) {
+            const question = this.options[index] ? this.$gettext('Wirklich löschen?') : true;
+            STUDIP.Dialog.confirm(question).done(() => {
+                this.$delete(this.options, index);
+            });
         },
-        onPaste: function (ev, position) {
-            let data = ev.clipboardData.getData("text").split("\n");
-            for (let i = 0; i < data.length; i++) {
-                if (data[i].trim()) {
-                    this.addOption(data[i], position + i);
-                }
-            }
+        onPaste(ev, position) {
+            ev.clipboardData
+                .getData('text')
+                .split("\n")
+                .filter(str => str.trim().length > 0)
+                .forEach((value, index) => this.addOption(value, position + index));
+            ev.preventDefault();
         },
         keyHandler(e, index) {
-            switch (e.keyCode) {
-                case 38: // up
-                    e.preventDefault();
-                    if (index > 0) {
-                        this.moveUp(index);
-                        this.$nextTick(function () {
-                            this.$refs['draghandle_' + (index - 1)][0].focus();
-                            this.assistiveLive = this.$gettextInterpolate(
-                                'Aktuelle Position in der Liste: %{pos} von %{listLength}.'
-                                , {pos: index, listLength: this.options.length}
-                            );
-                        });
-                    }
-                    break;
-                case 40: // down
-                    e.preventDefault();
-                    if (index < this.options.length - 1) {
-                        this.moveDown(index);
-                        this.$nextTick(function () {
-                            this.$refs['draghandle_' + (index + 1)][0].focus();
-                            this.assistiveLive = this.$gettextInterpolate(
-                                'Aktuelle Position in der Liste: %{pos} von %{listLength}.'
-                                , {pos: index + 2, listLength: this.options.length}
-                            );
-                        });
-                    }
-                    break;
-            }
-        },
-        moveDown: function (index) {
-            if (index == this.options.length - 1) {
+            if (e.keyCode !== 38 && e.keyCode !== 40) {
                 return;
             }
-            let option = this.options[index];
-            this.options[index] = this.options[index + 1];
-            this.options[index + 1] = option;
-            this.$forceUpdate();
+
+            e.preventDefault();
+
+            const moveUp = e.keyCode === 38;
+
+            this.moveElement(index, moveUp ? -1 : 1).then((newIndex) => {
+                this.assistiveLive = this.$gettextInterpolate(
+                    this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
+                    {pos: newIndex + 1, listLength: this.options.length}
+                );
+
+                this.$nextTick(() => {
+                    this.$refs['draghandle'][newIndex].focus();
+                });
+            })
         },
-        moveUp: function (index) {
-            if (index === 0) {
-                return;
+        moveElement(index, direction) {
+            if (this.options[index + direction] === undefined) {
+                return Promise.resolve(index);
             }
-            let option = this.options[index];
-            this.options[index] = this.options[index - 1];
-            this.options[index - 1] = option;
-            this.$forceUpdate();
+
+            const indices = [index, index + direction].sort();
+
+            this.options.splice(
+                Math.min(...indices),
+                2,
+                ...indices.reverse().map(idx => this.options[idx])
+            );
+
+            return Promise.resolve(index + direction);
         }
     },
-    mounted: function () {
-        this.options = this.value;
-        this.unique_id = 'array_input_' + Math.floor(Math.random() * 100000000);
-    },
     watch: {
-        options (new_data, old_data) {
-            if (typeof old_data === 'undefined' || typeof new_data === 'undefined') {
-                return;
-            }
-            this.$emit('input', new_data);
+        options: {
+            handler(current) {
+                this.$emit('input', current);
+            },
+            deep: true
         },
-        value (new_val) {
-            this.options = new_val;
+        value: {
+            handler(current) {
+                this.options = current;
+            },
+            immediate: true
         }
     }
 }
 </script>
-<style lang="scss" scoped>
-.input-array {
-    display: grid;
-    grid-template-areas:
-        "sr sr"
-        "options button";
-    grid-template-columns: calc(100% - 24px) 24px;
-    grid-template-rows: auto;
-
-    > .sr-only {
-        grid-area: sr;
-    }
-
-    > .options {
-        grid-area: options;
-    }
-
-    > button.as-link {
-        align-self: end;
-        grid-area: button;
-        justify-self: left;
-        margin-bottom: 8px;
-    }
+<style scoped>
+.input-array input[type="text"] {
+    max-width: unset;
 }
 </style>
diff --git a/resources/vue/components/questionnaires/LikertEdit.vue b/resources/vue/components/questionnaires/LikertEdit.vue
index c87f9fed982ad7d0a4f3264838d2f3b7973e71e1..736be6b93fd586911e4a87661c931e0b9db75513 100644
--- a/resources/vue/components/questionnaires/LikertEdit.vue
+++ b/resources/vue/components/questionnaires/LikertEdit.vue
@@ -1,66 +1,27 @@
 <template>
     <div class="likert_edit">
-
         <div class="formpart" tabindex="0" ref="autofocus">
             {{ $gettext('Einleitungstext' )}}
-            <studip-wysiwyg v-model="val_clone.description"></studip-wysiwyg>
+            <StudipWysiwyg v-model="val_clone.description" />
         </div>
 
-        <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
+        <InputArray v-model="val_clone.statements"
+                     :label="$gettext('Aussage')"
+                     :label-plural="$gettext('Aussagen')"
+                     :additional-colspan="val_clone.options.length"
+        >
+            <template #header-cells>
+                <th v-for="(option, index) in val_clone.options" class="option-cell" :key="index">
+                    {{ option }}
+                </th>
+            </template>
 
-        <table class="default nohover">
-            <thead>
-                <tr>
-                    <th class="dragcolumn"></th>
-                    <th>{{ $gettext('Aussagen') }}</th>
-                    <th v-for="(option, index) in val_clone.options" :key="index">{{ option }}</th>
-                    <th class="actions"></th>
-                </tr>
-            </thead>
-            <draggable v-model="val_clone.statements" handle=".dragarea" tag="tbody" class="statements">
-                <tr v-for="(statement, index) in val_clone.statements" :key="index">
-                    <td class="dragcolumn">
-                        <a class="dragarea"
-                           tabindex="0"
-                           :title="$gettextInterpolate($gettext('Sortierelement für Aussage %{statement}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {statement: statement})"
-                           @keydown="keyHandler($event, index)"
-                           :ref="'draghandle_' + index">
-                            <span class="drag-handle"></span>
-                        </a>
-                    </td>
-                    <td>
-                        <input type="text"
-                               :ref="'statement_' + index"
-                               :placeholder="$gettext('Aussage')"
-                               @paste="(ev) => onPaste(ev, index)"
-                               v-model="val_clone.statements[index]">
-                    </td>
-                    <td v-for="(option, index2) in val_clone.options" :key="index2">
-                        <input type="radio" disabled :title="option">
-                    </td>
-                    <td class="actions">
-                        <studip-icon name="delete"
-                                     shape="trash"
-                                     :size="20"
-                                     @click.prevent="deleteStatement(index)"
-                                     :title="$gettext('Aussage löschen')"
-                        ></studip-icon>
-                    </td>
-                </tr>
-            </draggable>
-            <tfoot>
-                <tr>
-                    <td :colspan="val_clone.options.length + 3">
-                        <studip-icon name="add"
-                                     shape="add"
-                                     :size="20"
-                                     @click.prevent="addStatement()"
-                                     :title="$gettext('Aussage hinzufügen')"
-                        ></studip-icon>
-                    </td>
-                </tr>
-            </tfoot>
-        </table>
+            <template #body-cells>
+                <td v-for="(option, index) in val_clone.options" class="option-cell" :key="index">
+                    <input type="radio" disabled :title="option">
+                </td>
+            </template>
+        </InputArray>
 
         <label>
             <input type="checkbox" v-model.number="val_clone.mandatory" true-value="1" false-value="0">
@@ -73,17 +34,18 @@
 
         <div>
             <div>{{ $gettext('Antwortmöglichkeiten konfigurieren') }}</div>
-            <input-array v-model="val_clone.options"></input-array>
+            <InputArray v-model="val_clone.options" />
         </div>
     </div>
 </template>
 
 <script>
-import draggable from 'vuedraggable';
-import InputArray from "./InputArray.vue";
 import { $gettext } from '../../../assets/javascripts/lib/gettext';
+import InputArray from "./InputArray.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
 
-const default_value = () => ({
+// This is necesssar since $gettext does not seem to work in data() or created()
+const default_values = () => ({
     description: '',
     statements: ['', '', '', ''],
     mandatory: 0,
@@ -96,115 +58,16 @@ const default_value = () => ({
         $gettext('trifft nicht zu'),
     ],
 });
+
 export default {
     name: 'likert-edit',
-    components: {
-        draggable,
-        InputArray
-    },
-    props: {
-        value: {
-            type: Object,
-            required: false,
-            default() {
-                return {...default_value()};
-            }
-        },
-        question_id: {
-            type: String,
-            required: false
-        }
-    },
-    data() {
-        return {
-            val_clone: null,
-            assistiveLive: ''
-        };
-    },
-    methods: {
-        addStatement(val = '', position = null) {
-            if (position === null) {
-                this.val_clone.statements.push(val || '');
-            } else {
-                this.val_clone.statements.splice(position, 0, val || '');
-            }
-            this.$nextTick(() => {
-                this.$refs['statement_' + (this.val_clone.statements.length - 1)][0].focus();
-            });
-        },
-        deleteStatement(index) {
-            STUDIP.Dialog.confirm(this.$gettext('Wirklich löschen?')).done(() => {
-                this.$delete(this.val_clone.statements, index);
-            });
-        },
-        onPaste(ev, position) {
-            let data = ev.clipboardData.getData("text").split("\n");
-            for (let i = 0; i < data.length; i++) {
-                if (data[i].trim()) {
-                    this.addStatement(data[i], position + i);
-                }
-            }
-        },
-        keyHandler(e, index) {
-            switch (e.keyCode) {
-                case 38: // up
-                    e.preventDefault();
-                    if (index > 0) {
-                        this.moveUp(index);
-                        this.$nextTick(() => {
-                            this.$refs['draghandle_' + (index - 1)][0].focus();
-                            this.assistiveLive = this.$gettextInterpolate(
-                                this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
-                                {pos: index, listLength: this.val_clone.statements.length}
-                            );
-                        });
-                    }
-                    break;
-                case 40: // down
-                    e.preventDefault();
-                    if (index < this.val_clone.statements.length - 1) {
-                        this.moveDown(index);
-                        this.$nextTick(() => {
-                            this.$refs['draghandle_' + (index + 1)][0].focus();
-                            this.assistiveLive = this.$gettextInterpolate(
-                                this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
-                                {pos: index + 2, listLength: this.val_clone.statements.length}
-                            );
-                        });
-                    }
-                    break;
-            }
-        },
-        moveDown(index) {
-            this.val_clone.statements.splice(
-                index,
-                2,
-                this.val_clone.statements[index + 1],
-                this.val_clone.statements[index]
-            )
-        },
-        moveUp(index) {
-            this.val_clone.statements.splice(
-                index - 1,
-                2,
-                this.val_clone.statements[index],
-                this.val_clone.statements[index - 1]
-            )
-        }
-    },
+    components: { InputArray },
+    mixins: [ QuestionnaireComponent ],
     created() {
-        this.val_clone = Object.assign({}, default_value(), this.value ?? {});
+        this.setDefaultValues(default_values());
     },
     mounted() {
         this.$refs.autofocus.focus();
-    },
-    watch: {
-        val_clone: {
-            handler(current) {
-                this.$emit('input', current);
-            },
-            deep: true
-        }
     }
 }
 </script>
diff --git a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue b/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
index bc5e82970fd72f4909ca2011020c49678214f8de..83d5fa2d5e877b95c9b8d38523086d1c7cd518c8 100644
--- a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
+++ b/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
@@ -8,39 +8,26 @@
 
         <div class="formpart">
             {{ $gettext('Hinweistext (optional)') }}
-            <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+            <StudipWysiwyg v-model="val_clone.description" />
         </div>
     </div>
 </template>
 
 <script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
 
 export default {
     name: 'questionnaire-info-edit',
-    components: {
-        StudipWysiwyg
+    mixins: [ QuestionnaireComponent ],
+    created() {
+        this.setDefaultValues({
+            url: '',
+            description: ''
+        });
     },
-    props: {
-        value: {
-            type: Object,
-            required: false,
-            default() {
-                return {
-                    url: '',
-                    description: ''
-                };
-            }
-        },
-        question_id: {
-            type: String,
-            required: false
-        }
-    },
-    data () {
-        return {
-            val_clone: this.value,
-        };
+    mounted() {
+        this.$refs.infoUrl.focus();
+        this.checkValidity();
     },
     methods: {
         checkValidity() {
@@ -53,15 +40,6 @@ export default {
                 this.$refs.infoUrl.reportValidity();
             }
         }
-    },
-    mounted() {
-        this.$refs.infoUrl.focus();
-        this.checkValidity();
-    },
-    watch: {
-        value (new_val) {
-            this.val_clone = new_val;
-        }
     }
 }
 </script>
diff --git a/resources/vue/components/questionnaires/RangescaleEdit.vue b/resources/vue/components/questionnaires/RangescaleEdit.vue
index 91aec1c24ed16a253d9d84126b981d9634f7be39..cd7ce3b215745987e0e4b1e7123c07a5520996a2 100644
--- a/resources/vue/components/questionnaires/RangescaleEdit.vue
+++ b/resources/vue/components/questionnaires/RangescaleEdit.vue
@@ -3,68 +3,25 @@
 
         <div class="formpart" tabindex="0" ref="autofocus">
             {{ $gettext('Einleitungstext') }}
-            <studip-wysiwyg v-model="val_clone.description"></studip-wysiwyg>
+            <StudipWysiwyg v-model="val_clone.description" />
         </div>
 
-        <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
-
-        <table class="default nohover">
-            <thead>
-                <tr>
-                    <th class="dragcolumn"></th>
-                    <th>{{ $gettext('Aussagen') }}</th>
-                    <th v-for="i in (val_clone.maximum - val_clone.minimum + 1)" :key="i" class="number">{{ (val_clone.minimum - 1 + i) }}</th>
-                    <th v-if="val_clone.alternative_answer.trim().length > 0">{{ val_clone.alternative_answer }}</th>
-                    <th class="actions"></th>
-                </tr>
-            </thead>
-            <draggable v-model="val_clone.statements" handle=".dragarea" tag="tbody" class="statements">
-                <tr v-for="(statement, index) in val_clone.statements" :key="index">
-                    <td class="dragcolumn">
-                        <a class="dragarea"
-                           tabindex="0"
-                           :title="$gettextInterpolate($gettext('Sortierelement für Aussage %{statement}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {statement: statement})"
-                           @keydown="keyHandler($event, index)"
-                           :ref="'draghandle_' + index">
-                            <span class="drag-handle"></span>
-                        </a>
-                    </td>
-                    <td>
-                        <input type="text"
-                               :ref="'statement_' + index"
-                               :placeholder="$gettext('Aussage')"
-                               @paste="(ev) => onPaste(ev, index)"
-                               v-model="val_clone.statements[index]">
-                    </td>
-                    <td v-for="i in (val_clone.maximum - val_clone.minimum + 1)" :key="i">
-                        <input type="radio" disabled :title="i + val_clone.minimum - 1">
-                    </td>
-                    <td v-if="val_clone.alternative_answer.trim().length > 0">
-                        <input type="radio" disabled :title="val_clone.alternative_answer">
-                    </td>
-                    <td class="actions">
-                        <studip-icon name="delete"
-                                     shape="trash"
-                                     :size="20"
-                                     @click.prevent="deleteStatement(index)"
-                                     :title="$gettext('Aussage löschen')"
-                        ></studip-icon>
-                    </td>
-                </tr>
-            </draggable>
-            <tfoot>
-                <tr>
-                    <td :colspan="val_clone.maximum - val_clone.minimum + 4 + (val_clone.alternative_answer.trim().length > 0 ? 1 : 0)">
-                        <studip-icon name="add"
-                                     shape="add"
-                                     :size="20"
-                                     @click.prevent="addStatement()"
-                                     :title="$gettext('Aussage hinzufügen')"
-                        ></studip-icon>
-                    </td>
-                </tr>
-            </tfoot>
-        </table>
+        <InputArray v-model="val_clone.statements"
+                     :label="$gettext('Aussage')"
+                     :label-plural="$gettext('Aussagen')"
+                     :additional-colspan="options.length"
+        >
+             <template #header-cells>
+                 <th v-for="(option, index) in options" class="option-cell" :key="index">
+                     {{ option }}
+                 </th>
+             </template>
+            <template #body-cells>
+                <td v-for="(option, index) in options" class="option-cell" :key="index">
+                    <input type="radio" disabled :title="option">
+                </td>
+            </template>
+        </InputArray>
 
         <label>
             <input type="checkbox" v-model.number="val_clone.mandatory" true-value="1" false-value="0">
@@ -82,135 +39,48 @@
 
         <label>
             {{ $gettext('Minimum') }}
-            <input type="number" v-model.number="val_clone.minimum" min="1">
+            <input type="number" v-model.number="val_clone.minimum" min="1" :max="val_clone.maximum">
         </label>
 
         <label>
             {{ $gettext('Ausweichantwort (leer lassen für keine)') }}
-            <input type="text" v-model="val_clone.alternative_answer">
+            <input type="text" v-model.trim="val_clone.alternative_answer">
         </label>
     </div>
 </template>
 
 <script>
-import draggable from 'vuedraggable';
+import InputArray from './InputArray.vue';
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
 
-const default_value = () => ({
-    description: '',
-    statements: ['', '', '', ''],
-    mandatory: 0,
-    randomize: 0,
-    minimum: 1,
-    maximum: 5,
-    alternative_answer: ''
-});
 export default {
-    name: 'likert-edit',
-    components: {
-        draggable,
-    },
-    props: {
-        value: {
-            type: Object,
-            required: false,
-            default() {
-                return default_value();
-            }
-        },
-        question_id: {
-            type: String,
-            required: false
-        }
-    },
-    data() {
-        return {
-            val_clone: null,
-            assistiveLive: ''
-        };
-    },
-    methods: {
-        addStatement(val = '', position = null) {
-            if (position === null) {
-                this.val_clone.statements.push(val || '');
-            } else {
-                this.val_clone.statements.splice(position, 0, val || '');
-            }
-            this.$nextTick(() => {
-                this.$refs['statement_' + (this.value.statements.length - 1)][0].focus();
-            });
-        },
-        deleteStatement(index) {
-            STUDIP.Dialog.confirm(this.$gettext('Wirklich löschen?')).done(() => {
-                this.$delete(this.value.statements, index);
-            });
-        },
-        onPaste(ev, position) {
-            let data = ev.clipboardData.getData('text').split("\n");
-            for (let i = 0; i < data.length; i++) {
-                if (data[i].trim()) {
-                    this.addStatement(data[i], position + i);
-                }
-            }
-        },
-        keyHandler(e, index) {
-            switch (e.keyCode) {
-                case 38: // up
-                    e.preventDefault();
-                    if (index > 0) {
-                        this.moveUp(index);
-                        this.$nextTick(() => {
-                            this.$refs['draghandle_' + (index - 1)][0].focus();
-                            this.assistiveLive = this.$gettextInterpolate(
-                                this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
-                                {pos: index, listLength: this.val_clone.statements.length}
-                            );
-                        });
-                    }
-                    break;
-                case 40: // down
-                    e.preventDefault();
-                    if (index < this.val_clone.statements.length - 1) {
-                        this.moveDown(index);
-                        this.$nextTick(() => {
-                            this.$refs['draghandle_' + (index + 1)][0].focus();
-                            this.assistiveLive = this.$gettextInterpolate(
-                                this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
-                                {pos: index + 2, listLength: this.val_clone.statements.length}
-                            );
-                        });
-                    }
-                    break;
-            }
-        },
-        moveDown(index) {
-            this.val_clone.statements.splice(
-                index,
-                2,
-                this.val_clone.statements[index + 1],
-                this.val_clone.statements[index]
-            );
-        },
-        moveUp(index) {
-            this.val_clone.statements.splice(
-                index - 1,
-                2,
-                this.val_clone.statements[index],
-                this.val_clone.statements[index - 1]
-            );
-        },
-    },
+    name: 'rangescale-edit',
+    components: { InputArray },
+    mixins: [ QuestionnaireComponent ],
     created() {
-        this.val_clone = Object.assign({}, default_value(), this.value ?? {});
+        this.setDefaultValues({
+            alternative_answer: '',
+            description: '',
+            mandatory: 0,
+            maximum: 5,
+            minimum: 1,
+            randomize: 0,
+            statements: ['', '', '', '']
+        });
     },
     mounted() {
         this.$refs.autofocus.focus();
     },
-    watch: {
-        val_clone: {
-            handler(current) {
-                this.$emit('input', current);
-            },
-            deep: true
+    computed: {
+        options() {
+            let result = [];
+            for (let i = this.val_clone.minimum; i <= this.val_clone.maximum; i += 1) {
+                result.push(i);
+            }
+            if (this.val_clone.alternative_answer.length > 0) {
+                result.push(this.val_clone.alternative_answer);
+            }
+            return result;
         }
     }
 }
diff --git a/resources/vue/components/questionnaires/VoteEdit.vue b/resources/vue/components/questionnaires/VoteEdit.vue
index 62de963e8c36a631a8cba1b660124a8cc1fe5a24..1d6d9cf7c066977535535e4c3b87cfcc52e714ec 100644
--- a/resources/vue/components/questionnaires/VoteEdit.vue
+++ b/resources/vue/components/questionnaires/VoteEdit.vue
@@ -2,10 +2,10 @@
     <div class="vote_edit">
         <div class="formpart" tabindex="0" ref="autofocus">
             {{ $gettext('Frage') }}
-            <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+            <StudipWysiwyg v-model="val_clone.description" />
         </div>
 
-        <input-array v-model="val_clone.options"></input-array>
+        <InputArray v-model="val_clone.options" />
 
         <label>
             <input type="checkbox" v-model.number="val_clone.multiplechoice" true-value="1" false-value="0">
@@ -24,47 +24,24 @@
 </template>
 
 <script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
 import InputArray from "./InputArray.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
 
 export default {
     name: 'vote-edit',
-    components: {
-        StudipWysiwyg,
-        InputArray
+    components: { InputArray },
+    mixins: [QuestionnaireComponent],
+    created() {
+        this.setDefaultValues({
+            description: '',
+            mandatory: '0',
+            multiplechoice: '1',
+            options: ['', '', '', ''],
+            randomize: '0',
+        });
     },
-    props: {
-        value: {
-            type: Object,
-            required: false,
-            default: function () {
-                return {};
-            }
-        },
-        question_id: {
-            type: String,
-            required: false
-        }
-    },
-    data: function () {
-        return {
-            val_clone: {}
-        };
-    },
-    mounted: function () {
-        this.val_clone = this.value;
-        if (!this.value.description) {
-            this.$emit('input', {
-                multiplechoice: 1,
-                options: ['', '', '', ''],
-            });
-        }
+    mounted() {
         this.$refs.autofocus.focus();
-    },
-    watch: {
-        value (new_val) {
-            this.val_clone = new_val;
-        }
     }
 }
 </script>
diff --git a/resources/vue/mixins/QuestionnaireComponent.js b/resources/vue/mixins/QuestionnaireComponent.js
new file mode 100644
index 0000000000000000000000000000000000000000..277f21c87dde745db62eee0063a2896be1d5ce27
--- /dev/null
+++ b/resources/vue/mixins/QuestionnaireComponent.js
@@ -0,0 +1,24 @@
+export const QuestionnaireComponent = {
+    props: {
+        value: Object
+    },
+    data () {
+        return {val_clone: this.value};
+    },
+    methods: {
+        setDefaultValues(value) {
+            this.val_clone = Object.assign(value, this.value);
+        }
+    },
+    watch: {
+        val_clone: {
+            handler(current) {
+                this.$emit('input', current);
+            },
+            deep: true
+        },
+        value (new_val) {
+            this.val_clone = new_val;
+        }
+    }
+};